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.
Files changed (71) hide show
  1. package/dist/client/connection.d.ts +27 -3
  2. package/dist/client/connection.d.ts.map +1 -1
  3. package/dist/client/connection.js +308 -56
  4. package/dist/client/connection.js.map +1 -1
  5. package/dist/client/http/request.d.ts +4 -0
  6. package/dist/client/http/request.d.ts.map +1 -1
  7. package/dist/client/http/request.js +135 -42
  8. package/dist/client/http/request.js.map +1 -1
  9. package/dist/client/http/response.d.ts.map +1 -1
  10. package/dist/client/http/response.js +3 -1
  11. package/dist/client/http/response.js.map +1 -1
  12. package/dist/client/index.d.ts +15 -7
  13. package/dist/client/index.d.ts.map +1 -1
  14. package/dist/client/index.js +105 -16
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/methods/advanced.d.ts.map +1 -1
  17. package/dist/client/methods/advanced.js.map +1 -1
  18. package/dist/client/methods/core.d.ts +24 -1
  19. package/dist/client/methods/core.d.ts.map +1 -1
  20. package/dist/client/methods/core.js +105 -0
  21. package/dist/client/methods/core.js.map +1 -1
  22. package/dist/client/methods/cron.d.ts.map +1 -1
  23. package/dist/client/methods/cron.js.map +1 -1
  24. package/dist/client/methods/dlq.d.ts.map +1 -1
  25. package/dist/client/methods/dlq.js.map +1 -1
  26. package/dist/client/methods/flows.d.ts.map +1 -1
  27. package/dist/client/methods/flows.js.map +1 -1
  28. package/dist/client/methods/jobs.d.ts.map +1 -1
  29. package/dist/client/methods/jobs.js.map +1 -1
  30. package/dist/client/methods/metrics.d.ts.map +1 -1
  31. package/dist/client/methods/metrics.js.map +1 -1
  32. package/dist/client/methods/queue.d.ts.map +1 -1
  33. package/dist/client/methods/queue.js.map +1 -1
  34. package/dist/client/types.d.ts +10 -3
  35. package/dist/client/types.d.ts.map +1 -1
  36. package/dist/errors.d.ts +105 -0
  37. package/dist/errors.d.ts.map +1 -0
  38. package/dist/errors.js +223 -0
  39. package/dist/errors.js.map +1 -0
  40. package/dist/events/subscriber.d.ts +1 -0
  41. package/dist/events/subscriber.d.ts.map +1 -1
  42. package/dist/events/subscriber.js +48 -7
  43. package/dist/events/subscriber.js.map +1 -1
  44. package/dist/events/types.d.ts +2 -0
  45. package/dist/events/types.d.ts.map +1 -1
  46. package/dist/hooks.d.ts +166 -0
  47. package/dist/hooks.d.ts.map +1 -0
  48. package/dist/hooks.js +73 -0
  49. package/dist/hooks.js.map +1 -0
  50. package/dist/index.d.ts +8 -1
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +35 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/queue.d.ts.map +1 -1
  55. package/dist/queue.js +9 -5
  56. package/dist/queue.js.map +1 -1
  57. package/dist/types.d.ts +53 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/utils/logger.d.ts +53 -0
  60. package/dist/utils/logger.d.ts.map +1 -0
  61. package/dist/utils/logger.js +150 -0
  62. package/dist/utils/logger.js.map +1 -0
  63. package/dist/utils/retry.d.ts +70 -0
  64. package/dist/utils/retry.d.ts.map +1 -0
  65. package/dist/utils/retry.js +149 -0
  66. package/dist/utils/retry.js.map +1 -0
  67. package/dist/worker.d.ts +26 -3
  68. package/dist/worker.d.ts.map +1 -1
  69. package/dist/worker.js +159 -56
  70. package/dist/worker.js.map +1 -1
  71. 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: Required<ClientOptions>;
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 buffer;
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
- get options(): Required<ClientOptions>;
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":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,UAAU,CAAC;AAI3D,eAAO,MAAM,iBAAiB,QAAc,CAAC;AAC7C,eAAO,MAAM,cAAc,OAAO,CAAC;AAKnC,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAKrD;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAKvD;AAQD,KAAK,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,QAAQ,CAAC;AAE/F,qBAAa,gBAAiB,SAAQ,YAAY;IAChD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAC;IAC5C,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,eAAe,CAAmC;IAC1D,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,WAAW,CAAS;gBAEhB,OAAO,GAAE,aAAkB;IAmBvC,IAAI,OAAO,IAAI,QAAQ,CAAC,aAAa,CAAC,CAA0B;IAE1D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAsD9B,OAAO,CAAC,iBAAiB;IAwBzB,OAAO,CAAC,mBAAmB;IAuB3B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,cAAc;IAUhB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B,WAAW,IAAI,OAAO;IACtB,kBAAkB,IAAI,eAAe;IAE/B,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IAOxB,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;YA0BrE,OAAO;YAmBP,QAAQ;CAiBvB"}
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 Error('Queue name is required');
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 Error(`Invalid queue name: "${queue}". Must be alphanumeric, _, -, . (1-256 chars)`);
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 Error(`Job data size (${size} bytes) exceeds max (${exports.MAX_JOB_DATA_SIZE} bytes)`);
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
- buffer = '';
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
- get options() { return this._options; }
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 = () => { this.removeListener('error', onError); resolve(); };
106
- const onError = (err) => { this.removeListener('connect', onConnect); reject(err); };
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 Error('Connection timeout'));
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 Error('Max reconnection attempts reached'));
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.buffer += data.toString();
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) => this.emit('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
- const lines = this.buffer.split('\n');
200
- this.buffer = lines.pop() ?? '';
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 { /* ignore */ }
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 { /* ignore */ }
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
- pending.reject(new Error(response.error));
233
- else
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 Error('Connection closed'));
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
- isConnected() { return this.connectionState === 'connected'; }
254
- getConnectionState() { return this.connectionState; }
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 Error('Authentication failed');
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
- if (this.connectionState === 'reconnecting') {
273
- await new Promise((resolve, reject) => {
274
- const timeout = setTimeout(() => {
275
- this.removeListener('reconnected', onReconnect);
276
- this.removeListener('reconnect_failed', onFailed);
277
- reject(new Error('Reconnection timeout'));
278
- }, this._options.timeout);
279
- const onReconnect = () => {
280
- clearTimeout(timeout);
281
- this.removeListener('reconnect_failed', onFailed);
282
- resolve();
283
- };
284
- const onFailed = (err) => {
285
- clearTimeout(timeout);
286
- this.removeListener('reconnected', onReconnect);
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 ? this.sendHttp(command, customTimeout) : this.sendTcp(command, customTimeout);
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 Error('Not connected');
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
- const reqId = generateReqId();
302
- const timeoutMs = customTimeout ?? this._options.timeout;
303
- const timer = setTimeout(() => { this.pendingRequests.delete(reqId); reject(new Error('Request timeout')); }, timeoutMs);
304
- this.pendingRequests.set(reqId, { resolve: (value) => resolve(value), reject, timer });
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)({ ...command, reqId });
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({ ...command, reqId }) + '\n');
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, { method: req.method, headers, body: req.body, signal: controller.signal });
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 Error('HTTP request timeout');
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 {