flashq 0.3.0 → 0.3.1
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/client/connection.d.ts +27 -3
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +308 -56
- package/dist/client/connection.js.map +1 -1
- package/dist/client/http/request.d.ts +4 -0
- package/dist/client/http/request.d.ts.map +1 -1
- package/dist/client/http/request.js +135 -42
- package/dist/client/http/request.js.map +1 -1
- package/dist/client/http/response.d.ts.map +1 -1
- package/dist/client/http/response.js +3 -1
- package/dist/client/http/response.js.map +1 -1
- package/dist/client/index.d.ts +15 -7
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +105 -16
- package/dist/client/index.js.map +1 -1
- package/dist/client/methods/advanced.d.ts.map +1 -1
- package/dist/client/methods/advanced.js.map +1 -1
- package/dist/client/methods/core.d.ts +24 -1
- package/dist/client/methods/core.d.ts.map +1 -1
- package/dist/client/methods/core.js +105 -0
- package/dist/client/methods/core.js.map +1 -1
- package/dist/client/methods/cron.d.ts.map +1 -1
- package/dist/client/methods/cron.js.map +1 -1
- package/dist/client/methods/dlq.d.ts.map +1 -1
- package/dist/client/methods/dlq.js.map +1 -1
- package/dist/client/methods/flows.d.ts.map +1 -1
- package/dist/client/methods/flows.js.map +1 -1
- package/dist/client/methods/jobs.d.ts.map +1 -1
- package/dist/client/methods/jobs.js.map +1 -1
- package/dist/client/methods/metrics.d.ts.map +1 -1
- package/dist/client/methods/metrics.js.map +1 -1
- package/dist/client/methods/queue.d.ts.map +1 -1
- package/dist/client/methods/queue.js.map +1 -1
- package/dist/client/types.d.ts +10 -3
- package/dist/client/types.d.ts.map +1 -1
- package/dist/errors.d.ts +105 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +223 -0
- package/dist/errors.js.map +1 -0
- package/dist/events/subscriber.d.ts +1 -0
- package/dist/events/subscriber.d.ts.map +1 -1
- package/dist/events/subscriber.js +48 -7
- package/dist/events/subscriber.js.map +1 -1
- package/dist/events/types.d.ts +2 -0
- package/dist/events/types.d.ts.map +1 -1
- package/dist/hooks.d.ts +166 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +73 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -1
- package/dist/index.js.map +1 -1
- package/dist/queue.d.ts.map +1 -1
- package/dist/queue.js +9 -5
- package/dist/queue.js.map +1 -1
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/logger.d.ts +53 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +150 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/retry.d.ts +70 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +149 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/worker.d.ts +26 -3
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +159 -56
- package/dist/worker.js.map +1 -1
- package/package.json +11 -1
|
@@ -1,23 +1,41 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
2
|
import type { ClientOptions } from '../types';
|
|
3
|
+
import { Logger } from '../utils/logger';
|
|
3
4
|
export declare const MAX_JOB_DATA_SIZE: number;
|
|
4
5
|
export declare const MAX_BATCH_SIZE = 1000;
|
|
5
6
|
export declare function validateQueueName(queue: string): void;
|
|
6
7
|
export declare function validateJobDataSize(data: unknown): void;
|
|
7
8
|
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'closed';
|
|
9
|
+
/** Client options with required fields except hooks */
|
|
10
|
+
type ResolvedClientOptions = Required<Omit<ClientOptions, 'hooks'>> & {
|
|
11
|
+
hooks?: ClientOptions['hooks'];
|
|
12
|
+
};
|
|
8
13
|
export declare class FlashQConnection extends EventEmitter {
|
|
9
|
-
protected _options:
|
|
14
|
+
protected _options: ResolvedClientOptions;
|
|
15
|
+
protected logger: Logger;
|
|
10
16
|
private socket;
|
|
11
17
|
private connectionState;
|
|
12
18
|
private authenticated;
|
|
13
19
|
private pendingRequests;
|
|
14
|
-
private
|
|
20
|
+
private bufferChunks;
|
|
21
|
+
private bufferRemainder;
|
|
15
22
|
private binaryBuffer;
|
|
16
23
|
private reconnectAttempts;
|
|
17
24
|
private reconnectTimer;
|
|
18
25
|
private manualClose;
|
|
26
|
+
private retryConfig;
|
|
27
|
+
private queueOnDisconnect;
|
|
28
|
+
private maxQueuedRequests;
|
|
29
|
+
private requestQueue;
|
|
30
|
+
private trackRequestIds;
|
|
31
|
+
private compression;
|
|
32
|
+
private compressionThreshold;
|
|
19
33
|
constructor(options?: ClientOptions);
|
|
20
|
-
|
|
34
|
+
/** @deprecated Use logger.debug() instead */
|
|
35
|
+
protected debug(message: string, data?: unknown): void;
|
|
36
|
+
/** Call connection hook */
|
|
37
|
+
private callConnectionHook;
|
|
38
|
+
get options(): ResolvedClientOptions;
|
|
21
39
|
connect(): Promise<void>;
|
|
22
40
|
private scheduleReconnect;
|
|
23
41
|
private setupSocketHandlers;
|
|
@@ -25,11 +43,17 @@ export declare class FlashQConnection extends EventEmitter {
|
|
|
25
43
|
private processBinaryBuffer;
|
|
26
44
|
private handleResponse;
|
|
27
45
|
close(): Promise<void>;
|
|
46
|
+
/** Get number of queued requests */
|
|
47
|
+
getQueuedRequestCount(): number;
|
|
28
48
|
isConnected(): boolean;
|
|
29
49
|
getConnectionState(): ConnectionState;
|
|
30
50
|
ping(): Promise<boolean>;
|
|
31
51
|
auth(token: string): Promise<void>;
|
|
32
52
|
send<T>(command: Record<string, unknown>, customTimeout?: number): Promise<T>;
|
|
53
|
+
private doSend;
|
|
54
|
+
private waitForReconnection;
|
|
55
|
+
private queueRequest;
|
|
56
|
+
private processRequestQueue;
|
|
33
57
|
private sendTcp;
|
|
34
58
|
private sendHttp;
|
|
35
59
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/client/connection.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/client/connection.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,aAAa,EAA4B,MAAM,UAAU,CAAC;AAWxE,OAAO,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAC;AAKxD,eAAO,MAAM,iBAAiB,QAAc,CAAC;AAC7C,eAAO,MAAM,cAAc,OAAO,CAAC;AAKnC,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAUrD;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAQvD;AAeD,KAAK,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,QAAQ,CAAC;AAS/F,uDAAuD;AACvD,KAAK,qBAAqB,GAAG,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,GAAG;IACpE,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CAChC,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,YAAY;IAChD,SAAS,CAAC,QAAQ,EAAE,qBAAqB,CAAC;IAC1C,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,eAAe,CAAmC;IAC1D,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,eAAe,CAAM;IAC7B,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAEjB;IACF,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,eAAe,CAAU;IACjC,OAAO,CAAC,WAAW,CAAU;IAC7B,OAAO,CAAC,oBAAoB,CAAS;gBAEzB,OAAO,GAAE,aAAkB;IA2DvC,6CAA6C;IAC7C,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI;IAItD,2BAA2B;IAC3B,OAAO,CAAC,kBAAkB;IAiB1B,IAAI,OAAO,IAAI,qBAAqB,CAEnC;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAmE9B,OAAO,CAAC,iBAAiB;IAqCzB,OAAO,CAAC,mBAAmB;IA2B3B,OAAO,CAAC,aAAa;IAoBrB,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,cAAc;IAuBhB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB5B,oCAAoC;IACpC,qBAAqB,IAAI,MAAM;IAI/B,WAAW,IAAI,OAAO;IAGtB,kBAAkB,IAAI,eAAe;IAI/B,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IASxB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOlC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;YAqBrE,MAAM;YAeN,mBAAmB;IAsBjC,OAAO,CAAC,YAAY;YAiBN,mBAAmB;YAenB,OAAO;YA6EP,QAAQ;CA0BvB"}
|
|
@@ -40,41 +40,70 @@ exports.validateJobDataSize = validateJobDataSize;
|
|
|
40
40
|
* Connection management for FlashQ client.
|
|
41
41
|
*/
|
|
42
42
|
const net = __importStar(require("net"));
|
|
43
|
+
const zlib = __importStar(require("zlib"));
|
|
44
|
+
const util_1 = require("util");
|
|
43
45
|
const events_1 = require("events");
|
|
44
46
|
const msgpack_1 = require("@msgpack/msgpack");
|
|
45
47
|
const request_1 = require("./http/request");
|
|
46
48
|
const response_1 = require("./http/response");
|
|
49
|
+
const errors_1 = require("../errors");
|
|
50
|
+
const retry_1 = require("../utils/retry");
|
|
51
|
+
const logger_1 = require("../utils/logger");
|
|
52
|
+
const hooks_1 = require("../hooks");
|
|
53
|
+
const gzip = (0, util_1.promisify)(zlib.gzip);
|
|
47
54
|
exports.MAX_JOB_DATA_SIZE = 1024 * 1024;
|
|
48
55
|
exports.MAX_BATCH_SIZE = 1000;
|
|
49
56
|
const QUEUE_NAME_REGEX = /^[a-zA-Z0-9_.-]{1,256}$/;
|
|
50
57
|
let requestIdCounter = 0;
|
|
51
58
|
const generateReqId = () => `r${++requestIdCounter}`;
|
|
52
59
|
function validateQueueName(queue) {
|
|
53
|
-
if (!queue || typeof queue !== 'string')
|
|
54
|
-
throw new
|
|
60
|
+
if (!queue || typeof queue !== 'string') {
|
|
61
|
+
throw new errors_1.ValidationError('Queue name is required', 'queue');
|
|
62
|
+
}
|
|
55
63
|
if (!QUEUE_NAME_REGEX.test(queue)) {
|
|
56
|
-
throw new
|
|
64
|
+
throw new errors_1.ValidationError(`Invalid queue name: "${queue}". Must be alphanumeric, _, -, . (1-256 chars)`, 'queue');
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
function validateJobDataSize(data) {
|
|
60
68
|
const size = JSON.stringify(data).length;
|
|
61
69
|
if (size > exports.MAX_JOB_DATA_SIZE) {
|
|
62
|
-
throw new
|
|
70
|
+
throw new errors_1.ValidationError(`Job data size (${size} bytes) exceeds max (${exports.MAX_JOB_DATA_SIZE} bytes)`, 'data');
|
|
63
71
|
}
|
|
64
72
|
}
|
|
73
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
74
|
+
enabled: false,
|
|
75
|
+
maxRetries: 3,
|
|
76
|
+
initialDelay: 100,
|
|
77
|
+
maxDelay: 5000,
|
|
78
|
+
};
|
|
65
79
|
class FlashQConnection extends events_1.EventEmitter {
|
|
66
80
|
_options;
|
|
81
|
+
logger;
|
|
67
82
|
socket = null;
|
|
68
83
|
connectionState = 'disconnected';
|
|
69
84
|
authenticated = false;
|
|
70
85
|
pendingRequests = new Map();
|
|
71
|
-
|
|
86
|
+
bufferChunks = [];
|
|
87
|
+
bufferRemainder = '';
|
|
72
88
|
binaryBuffer = Buffer.alloc(0);
|
|
73
89
|
reconnectAttempts = 0;
|
|
74
90
|
reconnectTimer = null;
|
|
75
91
|
manualClose = false;
|
|
92
|
+
retryConfig;
|
|
93
|
+
queueOnDisconnect;
|
|
94
|
+
maxQueuedRequests;
|
|
95
|
+
requestQueue = [];
|
|
96
|
+
trackRequestIds;
|
|
97
|
+
compression;
|
|
98
|
+
compressionThreshold;
|
|
76
99
|
constructor(options = {}) {
|
|
77
100
|
super();
|
|
101
|
+
// Determine log level: logLevel takes precedence, then debug flag
|
|
102
|
+
const logLevel = options.logLevel ?? (options.debug ? 'debug' : 'silent');
|
|
103
|
+
this.logger = new logger_1.Logger({
|
|
104
|
+
level: logLevel,
|
|
105
|
+
prefix: 'flashQ',
|
|
106
|
+
});
|
|
78
107
|
this._options = {
|
|
79
108
|
host: options.host ?? 'localhost',
|
|
80
109
|
port: options.port ?? 6789,
|
|
@@ -88,11 +117,61 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
88
117
|
maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
|
|
89
118
|
reconnectDelay: options.reconnectDelay ?? 1000,
|
|
90
119
|
maxReconnectDelay: options.maxReconnectDelay ?? 30000,
|
|
120
|
+
debug: options.debug ?? false,
|
|
121
|
+
logLevel: logLevel,
|
|
122
|
+
retry: options.retry ?? false,
|
|
123
|
+
queueOnDisconnect: options.queueOnDisconnect ?? false,
|
|
124
|
+
maxQueuedRequests: options.maxQueuedRequests ?? 100,
|
|
125
|
+
trackRequestIds: options.trackRequestIds ?? false,
|
|
126
|
+
compression: options.compression ?? false,
|
|
127
|
+
compressionThreshold: options.compressionThreshold ?? 1024,
|
|
128
|
+
hooks: options.hooks,
|
|
91
129
|
};
|
|
130
|
+
// Parse retry config
|
|
131
|
+
if (options.retry === true) {
|
|
132
|
+
this.retryConfig = { ...DEFAULT_RETRY_CONFIG, enabled: true };
|
|
133
|
+
}
|
|
134
|
+
else if (options.retry && typeof options.retry === 'object') {
|
|
135
|
+
this.retryConfig = {
|
|
136
|
+
enabled: options.retry.enabled ?? true,
|
|
137
|
+
maxRetries: options.retry.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries,
|
|
138
|
+
initialDelay: options.retry.initialDelay ?? DEFAULT_RETRY_CONFIG.initialDelay,
|
|
139
|
+
maxDelay: options.retry.maxDelay ?? DEFAULT_RETRY_CONFIG.maxDelay,
|
|
140
|
+
onRetry: options.retry.onRetry,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
this.retryConfig = { ...DEFAULT_RETRY_CONFIG };
|
|
145
|
+
}
|
|
146
|
+
this.queueOnDisconnect = options.queueOnDisconnect ?? false;
|
|
147
|
+
this.maxQueuedRequests = options.maxQueuedRequests ?? 100;
|
|
148
|
+
this.trackRequestIds = options.trackRequestIds ?? false;
|
|
149
|
+
this.compression = options.compression ?? false;
|
|
150
|
+
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
92
151
|
if (this._options.useHttp)
|
|
93
152
|
this.connectionState = 'connected';
|
|
94
153
|
}
|
|
95
|
-
|
|
154
|
+
/** @deprecated Use logger.debug() instead */
|
|
155
|
+
debug(message, data) {
|
|
156
|
+
this.logger.debug(message, data);
|
|
157
|
+
}
|
|
158
|
+
/** Call connection hook */
|
|
159
|
+
callConnectionHook(event, error, attempt) {
|
|
160
|
+
const hooks = this._options.hooks;
|
|
161
|
+
if (!hooks?.onConnection)
|
|
162
|
+
return;
|
|
163
|
+
const ctx = (0, hooks_1.createHookContext)({
|
|
164
|
+
host: this._options.host,
|
|
165
|
+
port: this._options.port,
|
|
166
|
+
event,
|
|
167
|
+
error,
|
|
168
|
+
attempt,
|
|
169
|
+
});
|
|
170
|
+
(0, hooks_1.callHook)(hooks.onConnection, ctx);
|
|
171
|
+
}
|
|
172
|
+
get options() {
|
|
173
|
+
return this._options;
|
|
174
|
+
}
|
|
96
175
|
async connect() {
|
|
97
176
|
if (this._options.useHttp) {
|
|
98
177
|
this.connectionState = 'connected';
|
|
@@ -102,8 +181,14 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
102
181
|
return;
|
|
103
182
|
if (this.connectionState === 'connecting') {
|
|
104
183
|
return new Promise((resolve, reject) => {
|
|
105
|
-
const onConnect = () => {
|
|
106
|
-
|
|
184
|
+
const onConnect = () => {
|
|
185
|
+
this.removeListener('error', onError);
|
|
186
|
+
resolve();
|
|
187
|
+
};
|
|
188
|
+
const onError = (err) => {
|
|
189
|
+
this.removeListener('connect', onConnect);
|
|
190
|
+
reject(err);
|
|
191
|
+
};
|
|
107
192
|
this.once('connect', onConnect);
|
|
108
193
|
this.once('error', onError);
|
|
109
194
|
});
|
|
@@ -113,7 +198,7 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
113
198
|
return new Promise((resolve, reject) => {
|
|
114
199
|
const timeout = setTimeout(() => {
|
|
115
200
|
this.connectionState = 'disconnected';
|
|
116
|
-
reject(new
|
|
201
|
+
reject(new errors_1.ConnectionError('Connection timeout', 'CONNECTION_TIMEOUT'));
|
|
117
202
|
}, this._options.timeout);
|
|
118
203
|
const opts = this._options.socketPath
|
|
119
204
|
? { path: this._options.socketPath }
|
|
@@ -123,12 +208,15 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
123
208
|
this.connectionState = 'connected';
|
|
124
209
|
this.reconnectAttempts = 0;
|
|
125
210
|
this.setupSocketHandlers();
|
|
211
|
+
this.logger.info('Connected to server', opts);
|
|
126
212
|
if (this._options.token) {
|
|
127
213
|
try {
|
|
128
214
|
await this.auth(this._options.token);
|
|
129
215
|
this.authenticated = true;
|
|
216
|
+
this.logger.info('Authenticated successfully');
|
|
130
217
|
}
|
|
131
218
|
catch (err) {
|
|
219
|
+
this.logger.error('Authentication failed', err);
|
|
132
220
|
this.socket?.destroy();
|
|
133
221
|
this.connectionState = 'disconnected';
|
|
134
222
|
reject(err);
|
|
@@ -136,6 +224,7 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
136
224
|
}
|
|
137
225
|
}
|
|
138
226
|
this.emit('connect');
|
|
227
|
+
this.callConnectionHook('connect');
|
|
139
228
|
resolve();
|
|
140
229
|
});
|
|
141
230
|
this.socket.on('error', (err) => {
|
|
@@ -152,7 +241,7 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
152
241
|
return;
|
|
153
242
|
if (this._options.maxReconnectAttempts > 0 &&
|
|
154
243
|
this.reconnectAttempts >= this._options.maxReconnectAttempts) {
|
|
155
|
-
this.emit('reconnect_failed', new
|
|
244
|
+
this.emit('reconnect_failed', new errors_1.ConnectionError('Max reconnection attempts reached', 'RECONNECTION_FAILED'));
|
|
156
245
|
return;
|
|
157
246
|
}
|
|
158
247
|
this.connectionState = 'reconnecting';
|
|
@@ -160,11 +249,17 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
160
249
|
const baseDelay = Math.min(this._options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this._options.maxReconnectDelay);
|
|
161
250
|
const delay = baseDelay + Math.random() * 0.3 * baseDelay;
|
|
162
251
|
this.emit('reconnecting', { attempt: this.reconnectAttempts, delay });
|
|
252
|
+
this.callConnectionHook('reconnecting', undefined, this.reconnectAttempts);
|
|
163
253
|
this.reconnectTimer = setTimeout(async () => {
|
|
164
254
|
try {
|
|
165
255
|
this.connectionState = 'disconnected';
|
|
166
256
|
await this.connect();
|
|
167
257
|
this.emit('reconnected');
|
|
258
|
+
this.callConnectionHook('reconnected');
|
|
259
|
+
// Process queued requests after successful reconnection
|
|
260
|
+
if (this.queueOnDisconnect) {
|
|
261
|
+
await this.processRequestQueue();
|
|
262
|
+
}
|
|
168
263
|
}
|
|
169
264
|
catch {
|
|
170
265
|
this.scheduleReconnect();
|
|
@@ -180,7 +275,7 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
180
275
|
this.processBinaryBuffer();
|
|
181
276
|
}
|
|
182
277
|
else {
|
|
183
|
-
this.
|
|
278
|
+
this.bufferChunks.push(data.toString());
|
|
184
279
|
this.processBuffer();
|
|
185
280
|
}
|
|
186
281
|
});
|
|
@@ -189,22 +284,35 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
189
284
|
this.connectionState = 'disconnected';
|
|
190
285
|
this.authenticated = false;
|
|
191
286
|
this.emit('disconnect');
|
|
287
|
+
this.callConnectionHook('disconnect');
|
|
192
288
|
if (wasConnected && !this.manualClose && this._options.autoReconnect) {
|
|
193
289
|
this.scheduleReconnect();
|
|
194
290
|
}
|
|
195
291
|
});
|
|
196
|
-
this.socket.on('error', (err) =>
|
|
292
|
+
this.socket.on('error', (err) => {
|
|
293
|
+
this.emit('error', err);
|
|
294
|
+
this.callConnectionHook('error', err);
|
|
295
|
+
});
|
|
197
296
|
}
|
|
198
297
|
processBuffer() {
|
|
199
|
-
|
|
200
|
-
this.
|
|
298
|
+
// Join chunks only when processing (more efficient than += on each chunk)
|
|
299
|
+
const fullBuffer = this.bufferRemainder + this.bufferChunks.join('');
|
|
300
|
+
this.bufferChunks.length = 0; // Clear array without reallocating
|
|
301
|
+
const lines = fullBuffer.split('\n');
|
|
302
|
+
this.bufferRemainder = lines.pop() ?? '';
|
|
201
303
|
for (const line of lines) {
|
|
202
304
|
if (!line.trim())
|
|
203
305
|
continue;
|
|
204
306
|
try {
|
|
205
307
|
this.handleResponse(JSON.parse(line));
|
|
206
308
|
}
|
|
207
|
-
catch {
|
|
309
|
+
catch (err) {
|
|
310
|
+
this.debug('Failed to parse JSON response', {
|
|
311
|
+
line,
|
|
312
|
+
error: err instanceof Error ? err.message : err,
|
|
313
|
+
});
|
|
314
|
+
this.emit('parse_error', err, line);
|
|
315
|
+
}
|
|
208
316
|
}
|
|
209
317
|
}
|
|
210
318
|
processBinaryBuffer() {
|
|
@@ -217,21 +325,34 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
217
325
|
try {
|
|
218
326
|
this.handleResponse((0, msgpack_1.decode)(frameData));
|
|
219
327
|
}
|
|
220
|
-
catch {
|
|
328
|
+
catch (err) {
|
|
329
|
+
this.debug('Failed to decode binary response', {
|
|
330
|
+
error: err instanceof Error ? err.message : err,
|
|
331
|
+
});
|
|
332
|
+
this.emit('parse_error', err, frameData);
|
|
333
|
+
}
|
|
221
334
|
}
|
|
222
335
|
}
|
|
223
336
|
handleResponse(response) {
|
|
224
|
-
if (!response.reqId)
|
|
337
|
+
if (!response.reqId) {
|
|
338
|
+
this.logger.warn('Received response without reqId', response);
|
|
225
339
|
return;
|
|
340
|
+
}
|
|
226
341
|
const pending = this.pendingRequests.get(response.reqId);
|
|
227
|
-
if (!pending)
|
|
342
|
+
if (!pending) {
|
|
343
|
+
this.logger.warn('Received response for unknown request', { reqId: response.reqId });
|
|
228
344
|
return;
|
|
345
|
+
}
|
|
229
346
|
this.pendingRequests.delete(response.reqId);
|
|
230
347
|
clearTimeout(pending.timer);
|
|
231
|
-
if (response.ok === false && response.error)
|
|
232
|
-
|
|
233
|
-
|
|
348
|
+
if (response.ok === false && response.error) {
|
|
349
|
+
this.logger.debug('Request failed', { reqId: response.reqId, error: response.error });
|
|
350
|
+
pending.reject((0, errors_1.parseServerError)(response.error, response.code));
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
this.logger.trace('Request succeeded', { reqId: response.reqId });
|
|
234
354
|
pending.resolve(response);
|
|
355
|
+
}
|
|
235
356
|
}
|
|
236
357
|
async close() {
|
|
237
358
|
this.manualClose = true;
|
|
@@ -240,18 +361,32 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
240
361
|
clearTimeout(this.reconnectTimer);
|
|
241
362
|
this.reconnectTimer = null;
|
|
242
363
|
}
|
|
364
|
+
// Reject pending requests
|
|
243
365
|
for (const [, pending] of this.pendingRequests) {
|
|
244
366
|
clearTimeout(pending.timer);
|
|
245
|
-
pending.reject(new
|
|
367
|
+
pending.reject(new errors_1.ConnectionError('Connection closed', 'CONNECTION_CLOSED'));
|
|
246
368
|
}
|
|
247
369
|
this.pendingRequests.clear();
|
|
370
|
+
// Reject queued requests
|
|
371
|
+
for (const req of this.requestQueue) {
|
|
372
|
+
req.reject(new errors_1.ConnectionError('Connection closed', 'CONNECTION_CLOSED'));
|
|
373
|
+
}
|
|
374
|
+
this.requestQueue = [];
|
|
248
375
|
if (this.socket) {
|
|
249
376
|
this.socket.destroy();
|
|
250
377
|
this.socket = null;
|
|
251
378
|
}
|
|
252
379
|
}
|
|
253
|
-
|
|
254
|
-
|
|
380
|
+
/** Get number of queued requests */
|
|
381
|
+
getQueuedRequestCount() {
|
|
382
|
+
return this.requestQueue.length;
|
|
383
|
+
}
|
|
384
|
+
isConnected() {
|
|
385
|
+
return this.connectionState === 'connected';
|
|
386
|
+
}
|
|
387
|
+
getConnectionState() {
|
|
388
|
+
return this.connectionState;
|
|
389
|
+
}
|
|
255
390
|
async ping() {
|
|
256
391
|
try {
|
|
257
392
|
const response = await this.send({ cmd: 'PING' });
|
|
@@ -264,54 +399,165 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
264
399
|
async auth(token) {
|
|
265
400
|
const response = await this.send({ cmd: 'AUTH', token });
|
|
266
401
|
if (!response.ok)
|
|
267
|
-
throw new
|
|
402
|
+
throw new errors_1.AuthenticationError();
|
|
268
403
|
this._options.token = token;
|
|
269
404
|
this.authenticated = true;
|
|
270
405
|
}
|
|
271
406
|
async send(command, customTimeout) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
reject(err);
|
|
288
|
-
};
|
|
289
|
-
this.once('reconnected', onReconnect);
|
|
290
|
-
this.once('reconnect_failed', onFailed);
|
|
407
|
+
// If retry is enabled, wrap the actual send with retry logic
|
|
408
|
+
if (this.retryConfig.enabled) {
|
|
409
|
+
return (0, retry_1.withRetry)(() => this.doSend(command, customTimeout), {
|
|
410
|
+
maxRetries: this.retryConfig.maxRetries,
|
|
411
|
+
initialDelay: this.retryConfig.initialDelay,
|
|
412
|
+
maxDelay: this.retryConfig.maxDelay,
|
|
413
|
+
onRetry: (error, attempt, delay) => {
|
|
414
|
+
this.debug('Retrying request', {
|
|
415
|
+
cmd: command.cmd,
|
|
416
|
+
attempt,
|
|
417
|
+
delay,
|
|
418
|
+
error: error.message,
|
|
419
|
+
});
|
|
420
|
+
this.retryConfig.onRetry?.(error, attempt, delay);
|
|
421
|
+
},
|
|
291
422
|
});
|
|
292
423
|
}
|
|
424
|
+
return this.doSend(command, customTimeout);
|
|
425
|
+
}
|
|
426
|
+
async doSend(command, customTimeout) {
|
|
427
|
+
// Queue request if disconnected and queueOnDisconnect is enabled
|
|
428
|
+
if (this.queueOnDisconnect && this.connectionState === 'reconnecting') {
|
|
429
|
+
return this.queueRequest(command, customTimeout);
|
|
430
|
+
}
|
|
431
|
+
if (this.connectionState === 'reconnecting') {
|
|
432
|
+
await this.waitForReconnection();
|
|
433
|
+
}
|
|
293
434
|
if (this.connectionState !== 'connected')
|
|
294
435
|
await this.connect();
|
|
295
|
-
return this._options.useHttp
|
|
436
|
+
return this._options.useHttp
|
|
437
|
+
? this.sendHttp(command, customTimeout)
|
|
438
|
+
: this.sendTcp(command, customTimeout);
|
|
439
|
+
}
|
|
440
|
+
async waitForReconnection() {
|
|
441
|
+
return new Promise((resolve, reject) => {
|
|
442
|
+
const timeout = setTimeout(() => {
|
|
443
|
+
this.removeListener('reconnected', onReconnect);
|
|
444
|
+
this.removeListener('reconnect_failed', onFailed);
|
|
445
|
+
reject(new errors_1.ConnectionError('Reconnection timeout', 'RECONNECTION_FAILED'));
|
|
446
|
+
}, this._options.timeout);
|
|
447
|
+
const onReconnect = () => {
|
|
448
|
+
clearTimeout(timeout);
|
|
449
|
+
this.removeListener('reconnect_failed', onFailed);
|
|
450
|
+
resolve();
|
|
451
|
+
};
|
|
452
|
+
const onFailed = (err) => {
|
|
453
|
+
clearTimeout(timeout);
|
|
454
|
+
this.removeListener('reconnected', onReconnect);
|
|
455
|
+
reject(err);
|
|
456
|
+
};
|
|
457
|
+
this.once('reconnected', onReconnect);
|
|
458
|
+
this.once('reconnect_failed', onFailed);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
queueRequest(command, customTimeout) {
|
|
462
|
+
if (this.requestQueue.length >= this.maxQueuedRequests) {
|
|
463
|
+
return Promise.reject(new errors_1.ConnectionError(`Request queue full (max: ${this.maxQueuedRequests})`, 'QUEUE_FULL'));
|
|
464
|
+
}
|
|
465
|
+
this.debug('Queueing request', { cmd: command.cmd, queueSize: this.requestQueue.length + 1 });
|
|
466
|
+
return new Promise((resolve, reject) => {
|
|
467
|
+
this.requestQueue.push({
|
|
468
|
+
command,
|
|
469
|
+
customTimeout,
|
|
470
|
+
resolve: resolve,
|
|
471
|
+
reject,
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
async processRequestQueue() {
|
|
476
|
+
if (this.requestQueue.length === 0)
|
|
477
|
+
return;
|
|
478
|
+
this.debug('Processing queued requests', { count: this.requestQueue.length });
|
|
479
|
+
const queue = [...this.requestQueue];
|
|
480
|
+
this.requestQueue = [];
|
|
481
|
+
for (const req of queue) {
|
|
482
|
+
try {
|
|
483
|
+
const result = await this.doSend(req.command, req.customTimeout);
|
|
484
|
+
req.resolve(result);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
req.reject(error instanceof Error ? error : new Error(String(error)));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
296
490
|
}
|
|
297
491
|
async sendTcp(command, customTimeout) {
|
|
298
|
-
if (!this.socket || this.connectionState !== 'connected')
|
|
299
|
-
throw new
|
|
492
|
+
if (!this.socket || this.connectionState !== 'connected') {
|
|
493
|
+
throw new errors_1.ConnectionError('Not connected', 'NOT_CONNECTED');
|
|
494
|
+
}
|
|
495
|
+
const reqId = generateReqId();
|
|
496
|
+
const timeoutMs = customTimeout ?? this._options.timeout;
|
|
497
|
+
// Set request ID for logger correlation if tracking is enabled
|
|
498
|
+
if (this.trackRequestIds) {
|
|
499
|
+
this.logger.setRequestId(reqId);
|
|
500
|
+
}
|
|
501
|
+
this.logger.trace('Preparing request', { reqId, cmd: command.cmd });
|
|
502
|
+
// Apply compression if enabled and payload is large enough
|
|
503
|
+
let payload = command;
|
|
504
|
+
let compressed = false;
|
|
505
|
+
if (this.compression && !this._options.useBinary) {
|
|
506
|
+
const jsonStr = JSON.stringify(command);
|
|
507
|
+
if (jsonStr.length >= this.compressionThreshold) {
|
|
508
|
+
try {
|
|
509
|
+
const compressedData = await gzip(Buffer.from(jsonStr));
|
|
510
|
+
payload = { ...command, _compressed: compressedData.toString('base64') };
|
|
511
|
+
compressed = true;
|
|
512
|
+
this.logger.trace('Payload compressed', {
|
|
513
|
+
reqId,
|
|
514
|
+
original: jsonStr.length,
|
|
515
|
+
compressed: compressedData.length,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
this.logger.warn('Compression failed, sending uncompressed', { reqId, error: err });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
300
523
|
return new Promise((resolve, reject) => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
524
|
+
this.logger.debug('Sending command', {
|
|
525
|
+
reqId,
|
|
526
|
+
cmd: command.cmd,
|
|
527
|
+
timeout: timeoutMs,
|
|
528
|
+
compressed,
|
|
529
|
+
});
|
|
530
|
+
const timer = setTimeout(() => {
|
|
531
|
+
this.pendingRequests.delete(reqId);
|
|
532
|
+
if (this.trackRequestIds)
|
|
533
|
+
this.logger.clearRequestId();
|
|
534
|
+
this.logger.warn('Request timeout', { reqId, cmd: command.cmd, timeout: timeoutMs });
|
|
535
|
+
reject(new errors_1.TimeoutError(`Request timeout for ${command.cmd}`, timeoutMs));
|
|
536
|
+
}, timeoutMs);
|
|
537
|
+
this.pendingRequests.set(reqId, {
|
|
538
|
+
resolve: (value) => {
|
|
539
|
+
if (this.trackRequestIds)
|
|
540
|
+
this.logger.clearRequestId();
|
|
541
|
+
resolve(value);
|
|
542
|
+
},
|
|
543
|
+
reject: (err) => {
|
|
544
|
+
if (this.trackRequestIds)
|
|
545
|
+
this.logger.clearRequestId();
|
|
546
|
+
reject(err);
|
|
547
|
+
},
|
|
548
|
+
timer,
|
|
549
|
+
});
|
|
305
550
|
if (this._options.useBinary) {
|
|
306
|
-
const encoded = (0, msgpack_1.encode)({ ...
|
|
551
|
+
const encoded = (0, msgpack_1.encode)({ ...payload, reqId });
|
|
307
552
|
const frame = Buffer.alloc(4 + encoded.length);
|
|
308
553
|
frame.writeUInt32BE(encoded.length, 0);
|
|
309
554
|
frame.set(encoded, 4);
|
|
310
555
|
this.socket.write(frame);
|
|
311
556
|
}
|
|
312
557
|
else {
|
|
313
|
-
this.socket.write(JSON.stringify({ ...
|
|
558
|
+
this.socket.write(JSON.stringify({ ...payload, reqId }) + '\n');
|
|
314
559
|
}
|
|
560
|
+
this.logger.trace('Command sent', { reqId });
|
|
315
561
|
});
|
|
316
562
|
}
|
|
317
563
|
async sendHttp(command, customTimeout) {
|
|
@@ -323,13 +569,19 @@ class FlashQConnection extends events_1.EventEmitter {
|
|
|
323
569
|
const controller = new AbortController();
|
|
324
570
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
325
571
|
try {
|
|
326
|
-
const res = await fetch(req.url, {
|
|
572
|
+
const res = await fetch(req.url, {
|
|
573
|
+
method: req.method,
|
|
574
|
+
headers,
|
|
575
|
+
body: req.body,
|
|
576
|
+
signal: controller.signal,
|
|
577
|
+
});
|
|
327
578
|
const json = (await res.json());
|
|
328
579
|
return (0, response_1.parseHttpResponse)(cmd, json);
|
|
329
580
|
}
|
|
330
581
|
catch (error) {
|
|
331
|
-
if (error instanceof Error && error.name === 'AbortError')
|
|
332
|
-
throw new
|
|
582
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
583
|
+
throw new errors_1.TimeoutError('HTTP request timeout', timeout);
|
|
584
|
+
}
|
|
333
585
|
throw error;
|
|
334
586
|
}
|
|
335
587
|
finally {
|