@ecodrix/erix-client 1.0.5 → 1.3.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/dist/index.d.ts +438 -8
- package/dist/index.js +732 -29
- package/dist/transports/HttpTransport.d.ts +49 -0
- package/dist/transports/HttpTransport.js +71 -0
- package/dist/transports/TransportLayer.d.ts +42 -0
- package/dist/transports/TransportLayer.js +9 -0
- package/dist/transports/WebSocketTransport.d.ts +86 -0
- package/dist/transports/WebSocketTransport.js +299 -0
- package/package.json +8 -4
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file HttpTransport.ts
|
|
3
|
+
* @module ErixClient/Transports/HttpTransport
|
|
4
|
+
*
|
|
5
|
+
* HTTP transport for @ecodrix/erix-client.
|
|
6
|
+
*
|
|
7
|
+
* Wraps the existing fetch-based request logic into a TransportLayer
|
|
8
|
+
* implementation. This is the default/fallback transport that preserves
|
|
9
|
+
* the original behavior of the client.
|
|
10
|
+
*/
|
|
11
|
+
import type { TransportLayer } from "./TransportLayer.js";
|
|
12
|
+
export interface HttpTransportOptions {
|
|
13
|
+
/** Full base URL of the erix-store instance */
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
/** Headers to include on every request (auth, tenant, content-type) */
|
|
16
|
+
headers: Record<string, string>;
|
|
17
|
+
/** Request timeout in milliseconds (default: 5000) */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class HttpTransport implements TransportLayer {
|
|
21
|
+
private readonly baseUrl;
|
|
22
|
+
private readonly headers;
|
|
23
|
+
private readonly timeoutMs;
|
|
24
|
+
private closed;
|
|
25
|
+
constructor(options: HttpTransportOptions);
|
|
26
|
+
/**
|
|
27
|
+
* HTTP is always "connected" — each request is independent.
|
|
28
|
+
*/
|
|
29
|
+
get isConnected(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Send a single HTTP request using native fetch.
|
|
32
|
+
*/
|
|
33
|
+
request(method: string, path: string, body?: unknown, params?: Record<string, string>): Promise<unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Pipeline over HTTP — executes requests sequentially since HTTP
|
|
36
|
+
* doesn't natively support multiplexing in a single connection.
|
|
37
|
+
* Still provides the same API contract as WebSocket pipeline.
|
|
38
|
+
*/
|
|
39
|
+
pipeline(requests: Array<{
|
|
40
|
+
method: string;
|
|
41
|
+
path: string;
|
|
42
|
+
body?: unknown;
|
|
43
|
+
params?: Record<string, string>;
|
|
44
|
+
}>): Promise<unknown[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Mark the transport as closed. Subsequent requests will throw.
|
|
47
|
+
*/
|
|
48
|
+
close(): void;
|
|
49
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file HttpTransport.ts
|
|
3
|
+
* @module ErixClient/Transports/HttpTransport
|
|
4
|
+
*
|
|
5
|
+
* HTTP transport for @ecodrix/erix-client.
|
|
6
|
+
*
|
|
7
|
+
* Wraps the existing fetch-based request logic into a TransportLayer
|
|
8
|
+
* implementation. This is the default/fallback transport that preserves
|
|
9
|
+
* the original behavior of the client.
|
|
10
|
+
*/
|
|
11
|
+
export class HttpTransport {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.closed = false;
|
|
14
|
+
this.baseUrl = options.baseUrl;
|
|
15
|
+
this.headers = options.headers;
|
|
16
|
+
this.timeoutMs = options.timeoutMs ?? 5000;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* HTTP is always "connected" — each request is independent.
|
|
20
|
+
*/
|
|
21
|
+
get isConnected() {
|
|
22
|
+
return !this.closed;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Send a single HTTP request using native fetch.
|
|
26
|
+
*/
|
|
27
|
+
async request(method, path, body, params) {
|
|
28
|
+
if (this.closed) {
|
|
29
|
+
throw new Error("[erix-store] HttpTransport has been closed");
|
|
30
|
+
}
|
|
31
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
32
|
+
if (params) {
|
|
33
|
+
for (const [k, v] of Object.entries(params)) {
|
|
34
|
+
url.searchParams.set(k, v);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(url.toString(), {
|
|
41
|
+
method,
|
|
42
|
+
headers: this.headers,
|
|
43
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
44
|
+
signal: controller.signal,
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const err = (await res.json().catch(() => ({})));
|
|
48
|
+
throw new Error(`[erix-store] ${method} ${path} → ${res.status}: ${err.error ?? res.statusText}`);
|
|
49
|
+
}
|
|
50
|
+
return await res.json();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pipeline over HTTP — executes requests sequentially since HTTP
|
|
58
|
+
* doesn't natively support multiplexing in a single connection.
|
|
59
|
+
* Still provides the same API contract as WebSocket pipeline.
|
|
60
|
+
*/
|
|
61
|
+
async pipeline(requests) {
|
|
62
|
+
// Execute all requests concurrently via Promise.all
|
|
63
|
+
return Promise.all(requests.map((r) => this.request(r.method, r.path, r.body, r.params)));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Mark the transport as closed. Subsequent requests will throw.
|
|
67
|
+
*/
|
|
68
|
+
close() {
|
|
69
|
+
this.closed = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file TransportLayer.ts
|
|
3
|
+
* @module ErixClient/Transports/TransportLayer
|
|
4
|
+
*
|
|
5
|
+
* Strategy interface for erix-client transports.
|
|
6
|
+
* Both HttpTransport and WebSocketTransport implement this interface,
|
|
7
|
+
* allowing the ErixClient to swap transports transparently.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Common interface that all transport implementations must satisfy.
|
|
11
|
+
* Provides request/response semantics regardless of underlying protocol.
|
|
12
|
+
*/
|
|
13
|
+
export interface TransportLayer {
|
|
14
|
+
/**
|
|
15
|
+
* Send a request and await the response.
|
|
16
|
+
* @param method HTTP-like method (GET, POST, DELETE, PATCH)
|
|
17
|
+
* @param path Route path (e.g. /core/get, /queue/v2/jobs)
|
|
18
|
+
* @param body Optional request body
|
|
19
|
+
* @param params Optional query parameters
|
|
20
|
+
*/
|
|
21
|
+
request(method: string, path: string, body?: unknown, params?: Record<string, string>): Promise<unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* Send a batch of requests as a single round-trip (pipeline).
|
|
24
|
+
* Returns an array of results in the same order as the input requests.
|
|
25
|
+
*/
|
|
26
|
+
pipeline(requests: Array<{
|
|
27
|
+
method: string;
|
|
28
|
+
path: string;
|
|
29
|
+
body?: unknown;
|
|
30
|
+
params?: Record<string, string>;
|
|
31
|
+
}>): Promise<unknown[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Close the transport and release resources.
|
|
34
|
+
*/
|
|
35
|
+
close(): void;
|
|
36
|
+
/**
|
|
37
|
+
* Whether the transport is currently connected/ready.
|
|
38
|
+
*/
|
|
39
|
+
readonly isConnected: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Transport mode option for ErixClient */
|
|
42
|
+
export type TransportMode = "http" | "ws" | "auto";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file TransportLayer.ts
|
|
3
|
+
* @module ErixClient/Transports/TransportLayer
|
|
4
|
+
*
|
|
5
|
+
* Strategy interface for erix-client transports.
|
|
6
|
+
* Both HttpTransport and WebSocketTransport implement this interface,
|
|
7
|
+
* allowing the ErixClient to swap transports transparently.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file WebSocketTransport.ts
|
|
3
|
+
* @module ErixClient/Transports/WebSocket
|
|
4
|
+
*
|
|
5
|
+
* WebSocket transport for @ecodrix/erix-client.
|
|
6
|
+
*
|
|
7
|
+
* Provides persistent binary (MessagePack) communication with erix-store,
|
|
8
|
+
* supporting request/response multiplexing via correlation IDs, pipeline
|
|
9
|
+
* batching, automatic reconnection with exponential backoff, and request
|
|
10
|
+
* buffering during disconnection.
|
|
11
|
+
*/
|
|
12
|
+
import type { TransportLayer } from "./TransportLayer.js";
|
|
13
|
+
export interface TransportOptions {
|
|
14
|
+
/** WebSocket URL, e.g. ws://localhost:9876 */
|
|
15
|
+
url: string;
|
|
16
|
+
/** Headers to send on connection (auth, tenant) */
|
|
17
|
+
headers: Record<string, string>;
|
|
18
|
+
/** Request timeout in milliseconds (default: 5000) */
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
/** Reconnection options */
|
|
21
|
+
reconnect?: {
|
|
22
|
+
/** Initial delay before first reconnect attempt (default: 1000) */
|
|
23
|
+
initialDelayMs?: number;
|
|
24
|
+
/** Maximum delay between reconnect attempts (default: 30000) */
|
|
25
|
+
maxDelayMs?: number;
|
|
26
|
+
/** Backoff multiplier (default: 2) */
|
|
27
|
+
backoffFactor?: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare class TimeoutError extends Error {
|
|
31
|
+
constructor(id: number, timeoutMs: number);
|
|
32
|
+
}
|
|
33
|
+
export declare class ConnectionLostError extends Error {
|
|
34
|
+
constructor();
|
|
35
|
+
}
|
|
36
|
+
export declare class TransportClosedError extends Error {
|
|
37
|
+
constructor();
|
|
38
|
+
}
|
|
39
|
+
export declare class WebSocketTransport implements TransportLayer {
|
|
40
|
+
private ws;
|
|
41
|
+
private pending;
|
|
42
|
+
private buffer;
|
|
43
|
+
private nextId;
|
|
44
|
+
private connected;
|
|
45
|
+
private closed;
|
|
46
|
+
private reconnectAttempt;
|
|
47
|
+
private reconnectTimer;
|
|
48
|
+
private readonly url;
|
|
49
|
+
private readonly headers;
|
|
50
|
+
private readonly timeoutMs;
|
|
51
|
+
private readonly initialDelayMs;
|
|
52
|
+
private readonly maxDelayMs;
|
|
53
|
+
private readonly backoffFactor;
|
|
54
|
+
constructor(options: TransportOptions);
|
|
55
|
+
/**
|
|
56
|
+
* Send a request and await the correlated response.
|
|
57
|
+
* If disconnected, the request is buffered and replayed on reconnect.
|
|
58
|
+
*/
|
|
59
|
+
request(method: string, path: string, body?: unknown, params?: Record<string, string>): Promise<unknown>;
|
|
60
|
+
/**
|
|
61
|
+
* Send a pipeline batch and await all responses.
|
|
62
|
+
* All operations are sent in a single frame and resolved together.
|
|
63
|
+
*/
|
|
64
|
+
pipeline(requests: Array<{
|
|
65
|
+
method: string;
|
|
66
|
+
path: string;
|
|
67
|
+
body?: unknown;
|
|
68
|
+
params?: Record<string, string>;
|
|
69
|
+
}>): Promise<unknown[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Close the transport. Rejects all pending requests, clears timers,
|
|
72
|
+
* and closes the WebSocket connection.
|
|
73
|
+
*/
|
|
74
|
+
close(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Returns whether the transport is currently connected.
|
|
77
|
+
*/
|
|
78
|
+
get isConnected(): boolean;
|
|
79
|
+
private connect;
|
|
80
|
+
private handleDisconnect;
|
|
81
|
+
private scheduleReconnect;
|
|
82
|
+
private replayBuffer;
|
|
83
|
+
private handleMessage;
|
|
84
|
+
private registerPending;
|
|
85
|
+
private registerPipelinePending;
|
|
86
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file WebSocketTransport.ts
|
|
3
|
+
* @module ErixClient/Transports/WebSocket
|
|
4
|
+
*
|
|
5
|
+
* WebSocket transport for @ecodrix/erix-client.
|
|
6
|
+
*
|
|
7
|
+
* Provides persistent binary (MessagePack) communication with erix-store,
|
|
8
|
+
* supporting request/response multiplexing via correlation IDs, pipeline
|
|
9
|
+
* batching, automatic reconnection with exponential backoff, and request
|
|
10
|
+
* buffering during disconnection.
|
|
11
|
+
*/
|
|
12
|
+
import { pack, unpack } from "msgpackr";
|
|
13
|
+
import WebSocket from "ws";
|
|
14
|
+
// ─── Errors ─────────────────────────────────────────────────────────────────
|
|
15
|
+
export class TimeoutError extends Error {
|
|
16
|
+
constructor(id, timeoutMs) {
|
|
17
|
+
super(`Request ${id} timed out after ${timeoutMs}ms`);
|
|
18
|
+
this.name = "TimeoutError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class ConnectionLostError extends Error {
|
|
22
|
+
constructor() {
|
|
23
|
+
super("WebSocket connection lost");
|
|
24
|
+
this.name = "ConnectionLostError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class TransportClosedError extends Error {
|
|
28
|
+
constructor() {
|
|
29
|
+
super("WebSocket transport has been closed");
|
|
30
|
+
this.name = "TransportClosedError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ─── WebSocketTransport ─────────────────────────────────────────────────────
|
|
34
|
+
export class WebSocketTransport {
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.ws = null;
|
|
37
|
+
this.pending = new Map();
|
|
38
|
+
this.buffer = [];
|
|
39
|
+
this.nextId = 1;
|
|
40
|
+
this.connected = false;
|
|
41
|
+
this.closed = false;
|
|
42
|
+
this.reconnectAttempt = 0;
|
|
43
|
+
this.reconnectTimer = null;
|
|
44
|
+
this.url = options.url;
|
|
45
|
+
this.headers = options.headers;
|
|
46
|
+
this.timeoutMs = options.timeoutMs ?? 5000;
|
|
47
|
+
this.initialDelayMs = options.reconnect?.initialDelayMs ?? 1000;
|
|
48
|
+
this.maxDelayMs = options.reconnect?.maxDelayMs ?? 30000;
|
|
49
|
+
this.backoffFactor = options.reconnect?.backoffFactor ?? 2;
|
|
50
|
+
this.connect();
|
|
51
|
+
}
|
|
52
|
+
// ─── Public API ─────────────────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Send a request and await the correlated response.
|
|
55
|
+
* If disconnected, the request is buffered and replayed on reconnect.
|
|
56
|
+
*/
|
|
57
|
+
request(method, path, body, params) {
|
|
58
|
+
if (this.closed) {
|
|
59
|
+
return Promise.reject(new TransportClosedError());
|
|
60
|
+
}
|
|
61
|
+
const id = this.nextId++;
|
|
62
|
+
const frame = { id, method, path };
|
|
63
|
+
if (body !== undefined)
|
|
64
|
+
frame.body = body;
|
|
65
|
+
if (params !== undefined)
|
|
66
|
+
frame.params = params;
|
|
67
|
+
const encoded = pack(frame);
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
|
|
70
|
+
this.registerPending(id, resolve, reject);
|
|
71
|
+
this.ws.send(encoded);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Buffer for replay on reconnect
|
|
75
|
+
this.buffer.push({ frame: encoded, resolve, reject });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Send a pipeline batch and await all responses.
|
|
81
|
+
* All operations are sent in a single frame and resolved together.
|
|
82
|
+
*/
|
|
83
|
+
pipeline(requests) {
|
|
84
|
+
if (this.closed) {
|
|
85
|
+
return Promise.reject(new TransportClosedError());
|
|
86
|
+
}
|
|
87
|
+
const pipelineId = this.nextId++;
|
|
88
|
+
const subRequests = requests.map((r) => {
|
|
89
|
+
const subId = this.nextId++;
|
|
90
|
+
const frame = { id: subId, method: r.method, path: r.path };
|
|
91
|
+
if (r.body !== undefined)
|
|
92
|
+
frame.body = r.body;
|
|
93
|
+
if (r.params !== undefined)
|
|
94
|
+
frame.params = r.params;
|
|
95
|
+
return frame;
|
|
96
|
+
});
|
|
97
|
+
const pipelineFrame = {
|
|
98
|
+
id: pipelineId,
|
|
99
|
+
pipeline: true,
|
|
100
|
+
requests: subRequests,
|
|
101
|
+
};
|
|
102
|
+
const encoded = pack(pipelineFrame);
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
if (this.connected && this.ws?.readyState === WebSocket.OPEN) {
|
|
105
|
+
this.registerPipelinePending(pipelineId, resolve, reject);
|
|
106
|
+
this.ws.send(encoded);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// Buffer pipeline request — wrap resolve/reject to handle pipeline response
|
|
110
|
+
const wrappedResolve = (data) => {
|
|
111
|
+
const pipelineResp = data;
|
|
112
|
+
resolve(pipelineResp.responses.map((r) => r.data));
|
|
113
|
+
};
|
|
114
|
+
this.buffer.push({ frame: encoded, resolve: wrappedResolve, reject });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Close the transport. Rejects all pending requests, clears timers,
|
|
120
|
+
* and closes the WebSocket connection.
|
|
121
|
+
*/
|
|
122
|
+
close() {
|
|
123
|
+
if (this.closed)
|
|
124
|
+
return;
|
|
125
|
+
this.closed = true;
|
|
126
|
+
// Clear reconnect timer
|
|
127
|
+
if (this.reconnectTimer !== null) {
|
|
128
|
+
clearTimeout(this.reconnectTimer);
|
|
129
|
+
this.reconnectTimer = null;
|
|
130
|
+
}
|
|
131
|
+
// Reject all pending requests
|
|
132
|
+
const closedError = new TransportClosedError();
|
|
133
|
+
for (const [_id, pending] of this.pending) {
|
|
134
|
+
clearTimeout(pending.timeout);
|
|
135
|
+
pending.reject(closedError);
|
|
136
|
+
}
|
|
137
|
+
this.pending.clear();
|
|
138
|
+
// Reject all buffered requests
|
|
139
|
+
for (const buffered of this.buffer) {
|
|
140
|
+
buffered.reject(closedError);
|
|
141
|
+
}
|
|
142
|
+
this.buffer = [];
|
|
143
|
+
// Close WebSocket
|
|
144
|
+
if (this.ws) {
|
|
145
|
+
this.ws.removeAllListeners();
|
|
146
|
+
if (this.ws.readyState === WebSocket.OPEN ||
|
|
147
|
+
this.ws.readyState === WebSocket.CONNECTING) {
|
|
148
|
+
this.ws.close(1000, "Client closing");
|
|
149
|
+
}
|
|
150
|
+
this.ws = null;
|
|
151
|
+
}
|
|
152
|
+
this.connected = false;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Returns whether the transport is currently connected.
|
|
156
|
+
*/
|
|
157
|
+
get isConnected() {
|
|
158
|
+
return this.connected;
|
|
159
|
+
}
|
|
160
|
+
// ─── Connection Management ──────────────────────────────────────────────
|
|
161
|
+
connect() {
|
|
162
|
+
if (this.closed)
|
|
163
|
+
return;
|
|
164
|
+
this.ws = new WebSocket(this.url, {
|
|
165
|
+
headers: this.headers,
|
|
166
|
+
});
|
|
167
|
+
this.ws.binaryType = "nodebuffer";
|
|
168
|
+
this.ws.on("open", () => {
|
|
169
|
+
this.connected = true;
|
|
170
|
+
this.reconnectAttempt = 0;
|
|
171
|
+
this.replayBuffer();
|
|
172
|
+
});
|
|
173
|
+
this.ws.on("message", (data) => {
|
|
174
|
+
this.handleMessage(data);
|
|
175
|
+
});
|
|
176
|
+
this.ws.on("close", () => {
|
|
177
|
+
this.handleDisconnect();
|
|
178
|
+
});
|
|
179
|
+
this.ws.on("error", () => {
|
|
180
|
+
// Error is followed by close event, so we handle reconnect there
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
handleDisconnect() {
|
|
184
|
+
this.connected = false;
|
|
185
|
+
if (this.closed)
|
|
186
|
+
return;
|
|
187
|
+
// Reject pending requests that were in-flight (they won't get responses)
|
|
188
|
+
const lostError = new ConnectionLostError();
|
|
189
|
+
for (const [_id, pending] of this.pending) {
|
|
190
|
+
clearTimeout(pending.timeout);
|
|
191
|
+
pending.reject(lostError);
|
|
192
|
+
}
|
|
193
|
+
this.pending.clear();
|
|
194
|
+
// Schedule reconnect with exponential backoff
|
|
195
|
+
this.scheduleReconnect();
|
|
196
|
+
}
|
|
197
|
+
scheduleReconnect() {
|
|
198
|
+
if (this.closed)
|
|
199
|
+
return;
|
|
200
|
+
const delay = Math.min(this.initialDelayMs * this.backoffFactor ** this.reconnectAttempt, this.maxDelayMs);
|
|
201
|
+
this.reconnectAttempt++;
|
|
202
|
+
this.reconnectTimer = setTimeout(() => {
|
|
203
|
+
this.reconnectTimer = null;
|
|
204
|
+
this.connect();
|
|
205
|
+
}, delay);
|
|
206
|
+
}
|
|
207
|
+
// ─── Buffer Replay ──────────────────────────────────────────────────────
|
|
208
|
+
replayBuffer() {
|
|
209
|
+
const buffered = this.buffer.splice(0);
|
|
210
|
+
for (const entry of buffered) {
|
|
211
|
+
if (this.closed) {
|
|
212
|
+
entry.reject(new TransportClosedError());
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Decode the frame to get the ID for pending registration
|
|
216
|
+
const decoded = unpack(entry.frame);
|
|
217
|
+
if ("pipeline" in decoded && decoded.pipeline) {
|
|
218
|
+
// Pipeline frame
|
|
219
|
+
const _pipelineResolve = (data) => {
|
|
220
|
+
const resp = data;
|
|
221
|
+
entry.resolve(resp);
|
|
222
|
+
};
|
|
223
|
+
this.registerPipelinePending(decoded.id, (responses) => {
|
|
224
|
+
entry.resolve(responses);
|
|
225
|
+
}, entry.reject);
|
|
226
|
+
this.ws?.send(entry.frame);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// Single request frame
|
|
230
|
+
this.registerPending(decoded.id, entry.resolve, entry.reject);
|
|
231
|
+
this.ws?.send(entry.frame);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// ─── Message Handling ───────────────────────────────────────────────────
|
|
236
|
+
handleMessage(data) {
|
|
237
|
+
let decoded;
|
|
238
|
+
try {
|
|
239
|
+
decoded = unpack(data);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// Malformed response — ignore
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (typeof decoded !== "object" || decoded === null)
|
|
246
|
+
return;
|
|
247
|
+
const msg = decoded;
|
|
248
|
+
// Check if it's a pipeline response
|
|
249
|
+
if (msg.pipeline === true && Array.isArray(msg.responses)) {
|
|
250
|
+
const pipelineResp = msg;
|
|
251
|
+
const pending = this.pending.get(pipelineResp.id);
|
|
252
|
+
if (pending) {
|
|
253
|
+
clearTimeout(pending.timeout);
|
|
254
|
+
this.pending.delete(pipelineResp.id);
|
|
255
|
+
pending.resolve(pipelineResp.responses.map((r) => r.data));
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Single response
|
|
260
|
+
const response = msg;
|
|
261
|
+
if (typeof response.id !== "number")
|
|
262
|
+
return;
|
|
263
|
+
const pending = this.pending.get(response.id);
|
|
264
|
+
if (pending) {
|
|
265
|
+
clearTimeout(pending.timeout);
|
|
266
|
+
this.pending.delete(response.id);
|
|
267
|
+
if (response.status >= 400) {
|
|
268
|
+
const errData = response.data;
|
|
269
|
+
const errMsg = typeof errData === "object" && errData?.error
|
|
270
|
+
? errData.error
|
|
271
|
+
: `Request failed with status ${response.status}`;
|
|
272
|
+
pending.reject(new Error(errMsg));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
pending.resolve(response.data);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// ─── Pending Request Registration ──────────────────────────────────────
|
|
280
|
+
registerPending(id, resolve, reject) {
|
|
281
|
+
const timeout = setTimeout(() => {
|
|
282
|
+
this.pending.delete(id);
|
|
283
|
+
reject(new TimeoutError(id, this.timeoutMs));
|
|
284
|
+
}, this.timeoutMs);
|
|
285
|
+
this.pending.set(id, { resolve, reject, timeout });
|
|
286
|
+
}
|
|
287
|
+
registerPipelinePending(id, resolve, reject) {
|
|
288
|
+
const timeout = setTimeout(() => {
|
|
289
|
+
this.pending.delete(id);
|
|
290
|
+
reject(new TimeoutError(id, this.timeoutMs));
|
|
291
|
+
}, this.timeoutMs);
|
|
292
|
+
// Store with a wrapped resolve that handles the pipeline response format
|
|
293
|
+
this.pending.set(id, {
|
|
294
|
+
resolve: resolve,
|
|
295
|
+
reject,
|
|
296
|
+
timeout,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecodrix/erix-client",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Type-safe HTTP client for
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Type-safe HTTP client for ERIX Store — serverless cache, queues, and real-time messaging. Zero dependencies, uses native fetch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"rate-limit",
|
|
23
23
|
"pubsub",
|
|
24
24
|
"ecodrix",
|
|
25
|
-
"
|
|
25
|
+
"serverless-data",
|
|
26
26
|
"in-memory"
|
|
27
27
|
],
|
|
28
28
|
"author": "dhanesh125",
|
|
@@ -30,9 +30,13 @@
|
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=18"
|
|
32
32
|
},
|
|
33
|
-
"dependencies": {
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"msgpackr": "^1.11.0",
|
|
35
|
+
"ws": "^8.18.0"
|
|
36
|
+
},
|
|
34
37
|
"devDependencies": {
|
|
35
38
|
"@types/node": "^20.12.7",
|
|
39
|
+
"@types/ws": "^8.5.10",
|
|
36
40
|
"typescript": "^5.4.5"
|
|
37
41
|
},
|
|
38
42
|
"repository": {
|