flashq 0.1.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.
Files changed (78) hide show
  1. package/README.md +283 -0
  2. package/dist/client/advanced.d.ts +174 -0
  3. package/dist/client/advanced.d.ts.map +1 -0
  4. package/dist/client/advanced.js +248 -0
  5. package/dist/client/advanced.js.map +1 -0
  6. package/dist/client/connection.d.ts +103 -0
  7. package/dist/client/connection.d.ts.map +1 -0
  8. package/dist/client/connection.js +570 -0
  9. package/dist/client/connection.js.map +1 -0
  10. package/dist/client/core.d.ts +119 -0
  11. package/dist/client/core.d.ts.map +1 -0
  12. package/dist/client/core.js +257 -0
  13. package/dist/client/core.js.map +1 -0
  14. package/dist/client/cron.d.ts +59 -0
  15. package/dist/client/cron.d.ts.map +1 -0
  16. package/dist/client/cron.js +82 -0
  17. package/dist/client/cron.js.map +1 -0
  18. package/dist/client/dlq.d.ts +52 -0
  19. package/dist/client/dlq.d.ts.map +1 -0
  20. package/dist/client/dlq.js +73 -0
  21. package/dist/client/dlq.js.map +1 -0
  22. package/dist/client/flows.d.ts +49 -0
  23. package/dist/client/flows.d.ts.map +1 -0
  24. package/dist/client/flows.js +67 -0
  25. package/dist/client/flows.js.map +1 -0
  26. package/dist/client/index.d.ts +644 -0
  27. package/dist/client/index.d.ts.map +1 -0
  28. package/dist/client/index.js +829 -0
  29. package/dist/client/index.js.map +1 -0
  30. package/dist/client/jobs.d.ts +183 -0
  31. package/dist/client/jobs.d.ts.map +1 -0
  32. package/dist/client/jobs.js +272 -0
  33. package/dist/client/jobs.js.map +1 -0
  34. package/dist/client/kv.d.ts +63 -0
  35. package/dist/client/kv.d.ts.map +1 -0
  36. package/dist/client/kv.js +131 -0
  37. package/dist/client/kv.js.map +1 -0
  38. package/dist/client/metrics.d.ts +34 -0
  39. package/dist/client/metrics.d.ts.map +1 -0
  40. package/dist/client/metrics.js +49 -0
  41. package/dist/client/metrics.js.map +1 -0
  42. package/dist/client/pubsub.d.ts +42 -0
  43. package/dist/client/pubsub.d.ts.map +1 -0
  44. package/dist/client/pubsub.js +92 -0
  45. package/dist/client/pubsub.js.map +1 -0
  46. package/dist/client/queue.d.ts +111 -0
  47. package/dist/client/queue.d.ts.map +1 -0
  48. package/dist/client/queue.js +160 -0
  49. package/dist/client/queue.js.map +1 -0
  50. package/dist/client/types.d.ts +23 -0
  51. package/dist/client/types.d.ts.map +1 -0
  52. package/dist/client/types.js +3 -0
  53. package/dist/client/types.js.map +1 -0
  54. package/dist/client.d.ts +17 -0
  55. package/dist/client.d.ts.map +1 -0
  56. package/dist/client.js +23 -0
  57. package/dist/client.js.map +1 -0
  58. package/dist/events.d.ts +184 -0
  59. package/dist/events.d.ts.map +1 -0
  60. package/dist/events.js +340 -0
  61. package/dist/events.js.map +1 -0
  62. package/dist/index.d.ts +30 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +43 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/queue.d.ts +104 -0
  67. package/dist/queue.d.ts.map +1 -0
  68. package/dist/queue.js +139 -0
  69. package/dist/queue.js.map +1 -0
  70. package/dist/types.d.ts +185 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +6 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/worker.d.ts +88 -0
  75. package/dist/worker.d.ts.map +1 -0
  76. package/dist/worker.js +296 -0
  77. package/dist/worker.js.map +1 -0
  78. package/package.json +70 -0
@@ -0,0 +1,103 @@
1
+ import { EventEmitter } from 'events';
2
+ import type { ClientOptions } from '../types';
3
+ /** Maximum job data size in bytes (1MB) */
4
+ export declare const MAX_JOB_DATA_SIZE: number;
5
+ /** Maximum batch size allowed by the server */
6
+ export declare const MAX_BATCH_SIZE = 1000;
7
+ /**
8
+ * Validate queue name to prevent injection attacks
9
+ * @throws Error if queue name is invalid
10
+ */
11
+ export declare function validateQueueName(queue: string): void;
12
+ /**
13
+ * Validate job data size
14
+ * @throws Error if data exceeds max size
15
+ */
16
+ export declare function validateJobDataSize(data: unknown): void;
17
+ type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'closed';
18
+ /**
19
+ * Base connection class for FlashQ.
20
+ * Handles TCP/HTTP connection, binary protocol, request multiplexing, and auto-reconnect.
21
+ */
22
+ export declare class FlashQConnection extends EventEmitter {
23
+ protected _options: Required<ClientOptions>;
24
+ private socket;
25
+ private connectionState;
26
+ private authenticated;
27
+ private pendingRequests;
28
+ private responseQueue;
29
+ private buffer;
30
+ private binaryBuffer;
31
+ private reconnectAttempts;
32
+ private reconnectTimer;
33
+ private manualClose;
34
+ constructor(options?: ClientOptions);
35
+ /** Get client options (read-only) */
36
+ get options(): Required<ClientOptions>;
37
+ /**
38
+ * Connect to FlashQ server.
39
+ * Automatically called on first command if not connected.
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const client = new FlashQ();
44
+ * await client.connect();
45
+ * ```
46
+ */
47
+ connect(): Promise<void>;
48
+ /**
49
+ * Attempt to reconnect with exponential backoff
50
+ */
51
+ private scheduleReconnect;
52
+ private setupSocketHandlers;
53
+ private processBuffer;
54
+ private processBinaryBuffer;
55
+ private handleResponse;
56
+ /**
57
+ * Close the connection to the server.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * await client.close();
62
+ * ```
63
+ */
64
+ close(): Promise<void>;
65
+ /**
66
+ * Check if connected to the server.
67
+ *
68
+ * @returns true if connected
69
+ */
70
+ isConnected(): boolean;
71
+ /**
72
+ * Get current connection state.
73
+ *
74
+ * @returns Connection state
75
+ */
76
+ getConnectionState(): ConnectionState;
77
+ /**
78
+ * Ping the server to check connection health.
79
+ *
80
+ * @returns true if server responds
81
+ */
82
+ ping(): Promise<boolean>;
83
+ /**
84
+ * Authenticate with the server.
85
+ *
86
+ * @param token - Authentication token
87
+ */
88
+ auth(token: string): Promise<void>;
89
+ /**
90
+ * Send a command to the server.
91
+ * Auto-connects if not connected.
92
+ *
93
+ * @param command - Command object
94
+ * @param customTimeout - Optional custom timeout
95
+ * @returns Response from server
96
+ */
97
+ send<T>(command: Record<string, unknown>, customTimeout?: number): Promise<T>;
98
+ private sendTcp;
99
+ private sendHttp;
100
+ private httpRequest;
101
+ }
102
+ export {};
103
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../src/client/connection.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,UAAU,CAAC;AAI3D,2CAA2C;AAC3C,eAAO,MAAM,iBAAiB,QAAc,CAAC;AAE7C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,OAAO,CAAC;AAWnC;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CASrD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAOvD;AAUD,KAAK,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,QAAQ,CAAC;AAI/F;;;GAGG;AACH,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,aAAa,CAGb;IACR,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,YAAY,CAA2B;IAG/C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,WAAW,CAAS;gBAEhB,OAAO,GAAE,aAAkB;IAkBvC,qCAAqC;IACrC,IAAI,OAAO,IAAI,QAAQ,CAAC,aAAa,CAAC,CAErC;IAED;;;;;;;;;OASG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAuE9B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAqCzB,OAAO,CAAC,mBAAmB;IA8B3B,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,cAAc;IAwBtB;;;;;;;OAOG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B5B;;;;OAIG;IACH,WAAW,IAAI,OAAO;IAItB;;;;OAIG;IACH,kBAAkB,IAAI,eAAe;IAIrC;;;;OAIG;IACG,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC;IAS9B;;;;OAIG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxC;;;;;;;OAOG;IACG,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;YAoCrE,OAAO;YAiCP,QAAQ;YAQR,WAAW;CAkI1B"}
@@ -0,0 +1,570 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FlashQConnection = exports.MAX_BATCH_SIZE = exports.MAX_JOB_DATA_SIZE = void 0;
37
+ exports.validateQueueName = validateQueueName;
38
+ exports.validateJobDataSize = validateJobDataSize;
39
+ /**
40
+ * Connection management for FlashQ client
41
+ * Production-ready with auto-reconnect, validation, and proper error handling
42
+ */
43
+ const net = __importStar(require("net"));
44
+ const events_1 = require("events");
45
+ const msgpack_1 = require("@msgpack/msgpack");
46
+ // ============== Constants ==============
47
+ /** Maximum job data size in bytes (1MB) */
48
+ exports.MAX_JOB_DATA_SIZE = 1024 * 1024;
49
+ /** Maximum batch size allowed by the server */
50
+ exports.MAX_BATCH_SIZE = 1000;
51
+ /** Valid queue name pattern */
52
+ const QUEUE_NAME_REGEX = /^[a-zA-Z0-9_.-]{1,256}$/;
53
+ /** Ultra-fast request ID generator (no crypto overhead) */
54
+ let requestIdCounter = 0;
55
+ const generateReqId = () => `r${++requestIdCounter}`;
56
+ // ============== Validation ==============
57
+ /**
58
+ * Validate queue name to prevent injection attacks
59
+ * @throws Error if queue name is invalid
60
+ */
61
+ function validateQueueName(queue) {
62
+ if (!queue || typeof queue !== 'string') {
63
+ throw new Error('Queue name is required');
64
+ }
65
+ if (!QUEUE_NAME_REGEX.test(queue)) {
66
+ throw new Error(`Invalid queue name: "${queue}". Must match pattern: alphanumeric, underscore, hyphen, dot (1-256 chars)`);
67
+ }
68
+ }
69
+ /**
70
+ * Validate job data size
71
+ * @throws Error if data exceeds max size
72
+ */
73
+ function validateJobDataSize(data) {
74
+ const size = JSON.stringify(data).length;
75
+ if (size > exports.MAX_JOB_DATA_SIZE) {
76
+ throw new Error(`Job data size (${size} bytes) exceeds maximum allowed (${exports.MAX_JOB_DATA_SIZE} bytes)`);
77
+ }
78
+ }
79
+ // ============== Connection Class ==============
80
+ /**
81
+ * Base connection class for FlashQ.
82
+ * Handles TCP/HTTP connection, binary protocol, request multiplexing, and auto-reconnect.
83
+ */
84
+ class FlashQConnection extends events_1.EventEmitter {
85
+ _options;
86
+ socket = null;
87
+ connectionState = 'disconnected';
88
+ authenticated = false;
89
+ pendingRequests = new Map();
90
+ responseQueue = [];
91
+ buffer = '';
92
+ binaryBuffer = Buffer.alloc(0);
93
+ // Reconnect state
94
+ reconnectAttempts = 0;
95
+ reconnectTimer = null;
96
+ manualClose = false;
97
+ constructor(options = {}) {
98
+ super();
99
+ this._options = {
100
+ host: options.host ?? 'localhost',
101
+ port: options.port ?? 6789,
102
+ httpPort: options.httpPort ?? 6790,
103
+ socketPath: options.socketPath ?? '',
104
+ token: options.token ?? '',
105
+ timeout: options.timeout ?? 5000,
106
+ useHttp: options.useHttp ?? false,
107
+ useBinary: options.useBinary ?? false,
108
+ autoReconnect: options.autoReconnect ?? true,
109
+ maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
110
+ reconnectDelay: options.reconnectDelay ?? 1000,
111
+ maxReconnectDelay: options.maxReconnectDelay ?? 30000,
112
+ };
113
+ }
114
+ /** Get client options (read-only) */
115
+ get options() {
116
+ return this._options;
117
+ }
118
+ /**
119
+ * Connect to FlashQ server.
120
+ * Automatically called on first command if not connected.
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * const client = new FlashQ();
125
+ * await client.connect();
126
+ * ```
127
+ */
128
+ async connect() {
129
+ if (this._options.useHttp) {
130
+ this.connectionState = 'connected';
131
+ return;
132
+ }
133
+ if (this.connectionState === 'connected') {
134
+ return;
135
+ }
136
+ if (this.connectionState === 'connecting') {
137
+ // Wait for existing connection attempt
138
+ return new Promise((resolve, reject) => {
139
+ const onConnect = () => {
140
+ this.removeListener('error', onError);
141
+ resolve();
142
+ };
143
+ const onError = (err) => {
144
+ this.removeListener('connect', onConnect);
145
+ reject(err);
146
+ };
147
+ this.once('connect', onConnect);
148
+ this.once('error', onError);
149
+ });
150
+ }
151
+ this.connectionState = 'connecting';
152
+ this.manualClose = false;
153
+ return new Promise((resolve, reject) => {
154
+ const timeout = setTimeout(() => {
155
+ this.connectionState = 'disconnected';
156
+ reject(new Error('Connection timeout'));
157
+ }, this._options.timeout);
158
+ const connectionOptions = this._options.socketPath
159
+ ? { path: this._options.socketPath }
160
+ : { host: this._options.host, port: this._options.port };
161
+ this.socket = net.createConnection(connectionOptions, async () => {
162
+ clearTimeout(timeout);
163
+ this.connectionState = 'connected';
164
+ this.reconnectAttempts = 0;
165
+ this.setupSocketHandlers();
166
+ if (this._options.token) {
167
+ try {
168
+ await this.auth(this._options.token);
169
+ this.authenticated = true;
170
+ }
171
+ catch (err) {
172
+ this.socket?.destroy();
173
+ this.connectionState = 'disconnected';
174
+ reject(err);
175
+ return;
176
+ }
177
+ }
178
+ this.emit('connect');
179
+ resolve();
180
+ });
181
+ this.socket.on('error', (err) => {
182
+ clearTimeout(timeout);
183
+ if (this.connectionState === 'connecting') {
184
+ this.connectionState = 'disconnected';
185
+ reject(err);
186
+ }
187
+ });
188
+ });
189
+ }
190
+ /**
191
+ * Attempt to reconnect with exponential backoff
192
+ */
193
+ scheduleReconnect() {
194
+ if (this.manualClose || !this._options.autoReconnect) {
195
+ return;
196
+ }
197
+ if (this._options.maxReconnectAttempts > 0 &&
198
+ this.reconnectAttempts >= this._options.maxReconnectAttempts) {
199
+ this.emit('reconnect_failed', new Error('Max reconnection attempts reached'));
200
+ return;
201
+ }
202
+ this.connectionState = 'reconnecting';
203
+ this.reconnectAttempts++;
204
+ // Exponential backoff with jitter
205
+ const baseDelay = Math.min(this._options.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this._options.maxReconnectDelay);
206
+ const jitter = Math.random() * 0.3 * baseDelay;
207
+ const delay = baseDelay + jitter;
208
+ this.emit('reconnecting', { attempt: this.reconnectAttempts, delay });
209
+ this.reconnectTimer = setTimeout(async () => {
210
+ try {
211
+ this.connectionState = 'disconnected';
212
+ await this.connect();
213
+ this.emit('reconnected');
214
+ }
215
+ catch {
216
+ this.scheduleReconnect();
217
+ }
218
+ }, delay);
219
+ }
220
+ setupSocketHandlers() {
221
+ if (!this.socket)
222
+ return;
223
+ this.socket.on('data', (data) => {
224
+ if (this._options.useBinary) {
225
+ this.binaryBuffer = Buffer.concat([this.binaryBuffer, data]);
226
+ this.processBinaryBuffer();
227
+ }
228
+ else {
229
+ this.buffer += data.toString();
230
+ this.processBuffer();
231
+ }
232
+ });
233
+ this.socket.on('close', () => {
234
+ const wasConnected = this.connectionState === 'connected';
235
+ this.connectionState = 'disconnected';
236
+ this.authenticated = false;
237
+ this.emit('disconnect');
238
+ // Auto-reconnect if enabled and not manually closed
239
+ if (wasConnected && !this.manualClose && this._options.autoReconnect) {
240
+ this.scheduleReconnect();
241
+ }
242
+ });
243
+ this.socket.on('error', (err) => {
244
+ this.emit('error', err);
245
+ });
246
+ }
247
+ processBuffer() {
248
+ const lines = this.buffer.split('\n');
249
+ this.buffer = lines.pop() ?? '';
250
+ for (const line of lines) {
251
+ if (!line.trim())
252
+ continue;
253
+ try {
254
+ const response = JSON.parse(line);
255
+ this.handleResponse(response);
256
+ }
257
+ catch {
258
+ // Ignore parse errors
259
+ }
260
+ }
261
+ }
262
+ processBinaryBuffer() {
263
+ while (this.binaryBuffer.length >= 4) {
264
+ const len = this.binaryBuffer.readUInt32BE(0);
265
+ if (this.binaryBuffer.length < 4 + len)
266
+ break;
267
+ const frameData = this.binaryBuffer.subarray(4, 4 + len);
268
+ this.binaryBuffer = this.binaryBuffer.subarray(4 + len);
269
+ try {
270
+ const response = (0, msgpack_1.decode)(frameData);
271
+ this.handleResponse(response);
272
+ }
273
+ catch {
274
+ // Ignore decode errors
275
+ }
276
+ }
277
+ }
278
+ handleResponse(response) {
279
+ if (response.reqId) {
280
+ const pending = this.pendingRequests.get(response.reqId);
281
+ if (pending) {
282
+ this.pendingRequests.delete(response.reqId);
283
+ clearTimeout(pending.timer);
284
+ if (response.ok === false && response.error) {
285
+ pending.reject(new Error(response.error));
286
+ }
287
+ else {
288
+ pending.resolve(response);
289
+ }
290
+ }
291
+ }
292
+ else {
293
+ const pending = this.responseQueue.shift();
294
+ if (pending) {
295
+ if (response.ok === false && response.error) {
296
+ pending.reject(new Error(response.error));
297
+ }
298
+ else {
299
+ pending.resolve(response);
300
+ }
301
+ }
302
+ }
303
+ }
304
+ /**
305
+ * Close the connection to the server.
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * await client.close();
310
+ * ```
311
+ */
312
+ async close() {
313
+ this.manualClose = true;
314
+ this.connectionState = 'closed';
315
+ // Cancel any pending reconnect
316
+ if (this.reconnectTimer) {
317
+ clearTimeout(this.reconnectTimer);
318
+ this.reconnectTimer = null;
319
+ }
320
+ for (const [, pending] of this.pendingRequests) {
321
+ clearTimeout(pending.timer);
322
+ pending.reject(new Error('Connection closed'));
323
+ }
324
+ this.pendingRequests.clear();
325
+ for (const pending of this.responseQueue) {
326
+ pending.reject(new Error('Connection closed'));
327
+ }
328
+ this.responseQueue.length = 0;
329
+ if (this.socket) {
330
+ this.socket.destroy();
331
+ this.socket = null;
332
+ }
333
+ }
334
+ /**
335
+ * Check if connected to the server.
336
+ *
337
+ * @returns true if connected
338
+ */
339
+ isConnected() {
340
+ return this.connectionState === 'connected';
341
+ }
342
+ /**
343
+ * Get current connection state.
344
+ *
345
+ * @returns Connection state
346
+ */
347
+ getConnectionState() {
348
+ return this.connectionState;
349
+ }
350
+ /**
351
+ * Ping the server to check connection health.
352
+ *
353
+ * @returns true if server responds
354
+ */
355
+ async ping() {
356
+ try {
357
+ const response = await this.send({ cmd: 'PING' });
358
+ return response.ok || response.pong === true;
359
+ }
360
+ catch {
361
+ return false;
362
+ }
363
+ }
364
+ /**
365
+ * Authenticate with the server.
366
+ *
367
+ * @param token - Authentication token
368
+ */
369
+ async auth(token) {
370
+ const response = await this.send({
371
+ cmd: 'AUTH',
372
+ token,
373
+ });
374
+ if (!response.ok) {
375
+ throw new Error('Authentication failed');
376
+ }
377
+ this._options.token = token;
378
+ this.authenticated = true;
379
+ }
380
+ /**
381
+ * Send a command to the server.
382
+ * Auto-connects if not connected.
383
+ *
384
+ * @param command - Command object
385
+ * @param customTimeout - Optional custom timeout
386
+ * @returns Response from server
387
+ */
388
+ async send(command, customTimeout) {
389
+ // Wait for reconnection if in progress
390
+ if (this.connectionState === 'reconnecting') {
391
+ await new Promise((resolve, reject) => {
392
+ const timeout = setTimeout(() => {
393
+ this.removeListener('reconnected', onReconnect);
394
+ this.removeListener('reconnect_failed', onFailed);
395
+ reject(new Error('Reconnection timeout'));
396
+ }, this._options.timeout);
397
+ const onReconnect = () => {
398
+ clearTimeout(timeout);
399
+ this.removeListener('reconnect_failed', onFailed);
400
+ resolve();
401
+ };
402
+ const onFailed = (err) => {
403
+ clearTimeout(timeout);
404
+ this.removeListener('reconnected', onReconnect);
405
+ reject(err);
406
+ };
407
+ this.once('reconnected', onReconnect);
408
+ this.once('reconnect_failed', onFailed);
409
+ });
410
+ }
411
+ if (this.connectionState !== 'connected') {
412
+ await this.connect();
413
+ }
414
+ if (this._options.useHttp) {
415
+ return this.sendHttp(command, customTimeout);
416
+ }
417
+ return this.sendTcp(command, customTimeout);
418
+ }
419
+ async sendTcp(command, customTimeout) {
420
+ if (!this.socket || this.connectionState !== 'connected') {
421
+ throw new Error('Not connected');
422
+ }
423
+ return new Promise((resolve, reject) => {
424
+ const reqId = generateReqId();
425
+ const timeoutMs = customTimeout ?? this._options.timeout;
426
+ const timer = setTimeout(() => {
427
+ this.pendingRequests.delete(reqId);
428
+ reject(new Error('Request timeout'));
429
+ }, timeoutMs);
430
+ this.pendingRequests.set(reqId, {
431
+ resolve: (value) => resolve(value),
432
+ reject,
433
+ timer,
434
+ });
435
+ if (this._options.useBinary) {
436
+ const payload = { ...command, reqId };
437
+ const encoded = (0, msgpack_1.encode)(payload);
438
+ const frame = Buffer.alloc(4 + encoded.length);
439
+ frame.writeUInt32BE(encoded.length, 0);
440
+ frame.set(encoded, 4);
441
+ this.socket.write(frame);
442
+ }
443
+ else {
444
+ this.socket.write(JSON.stringify({ ...command, reqId }) + '\n');
445
+ }
446
+ });
447
+ }
448
+ async sendHttp(command, customTimeout) {
449
+ const { cmd, ...params } = command;
450
+ const baseUrl = `http://${this._options.host}:${this._options.httpPort}`;
451
+ const timeout = customTimeout ?? this._options.timeout;
452
+ const response = await this.httpRequest(baseUrl, cmd, params, timeout);
453
+ return response;
454
+ }
455
+ async httpRequest(baseUrl, cmd, params, timeout) {
456
+ const headers = {
457
+ 'Content-Type': 'application/json',
458
+ };
459
+ if (this._options.token) {
460
+ headers['Authorization'] = `Bearer ${this._options.token}`;
461
+ }
462
+ // URL-encode queue names to prevent injection
463
+ const encodeQueue = (queue) => encodeURIComponent(String(queue));
464
+ let url;
465
+ let method;
466
+ let body;
467
+ switch (cmd) {
468
+ case 'PUSH':
469
+ url = `${baseUrl}/queues/${encodeQueue(params.queue)}/jobs`;
470
+ method = 'POST';
471
+ body = JSON.stringify({
472
+ data: params.data,
473
+ priority: params.priority,
474
+ delay: params.delay,
475
+ ttl: params.ttl,
476
+ timeout: params.timeout,
477
+ max_attempts: params.max_attempts,
478
+ backoff: params.backoff,
479
+ unique_key: params.unique_key,
480
+ depends_on: params.depends_on,
481
+ tags: params.tags,
482
+ lifo: params.lifo,
483
+ remove_on_complete: params.remove_on_complete,
484
+ remove_on_fail: params.remove_on_fail,
485
+ stall_timeout: params.stall_timeout,
486
+ debounce_id: params.debounce_id,
487
+ debounce_ttl: params.debounce_ttl,
488
+ job_id: params.job_id,
489
+ keep_completed_age: params.keep_completed_age,
490
+ keep_completed_count: params.keep_completed_count,
491
+ });
492
+ break;
493
+ case 'PULL':
494
+ url = `${baseUrl}/queues/${encodeQueue(params.queue)}/jobs?count=1`;
495
+ method = 'GET';
496
+ break;
497
+ case 'PULLB':
498
+ url = `${baseUrl}/queues/${encodeQueue(params.queue)}/jobs?count=${params.count}`;
499
+ method = 'GET';
500
+ break;
501
+ case 'ACK':
502
+ url = `${baseUrl}/jobs/${params.id}/ack`;
503
+ method = 'POST';
504
+ body = JSON.stringify({ result: params.result });
505
+ break;
506
+ case 'FAIL':
507
+ url = `${baseUrl}/jobs/${params.id}/fail`;
508
+ method = 'POST';
509
+ body = JSON.stringify({ error: params.error });
510
+ break;
511
+ case 'STATS':
512
+ url = `${baseUrl}/stats`;
513
+ method = 'GET';
514
+ break;
515
+ case 'METRICS':
516
+ url = `${baseUrl}/metrics`;
517
+ method = 'GET';
518
+ break;
519
+ case 'LISTQUEUES':
520
+ url = `${baseUrl}/queues`;
521
+ method = 'GET';
522
+ break;
523
+ default:
524
+ throw new Error(`HTTP not supported for command: ${cmd}`);
525
+ }
526
+ // Create abort controller for timeout
527
+ const controller = new AbortController();
528
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
529
+ try {
530
+ const res = await fetch(url, { method, headers, body, signal: controller.signal });
531
+ const json = (await res.json());
532
+ if (!json.ok) {
533
+ throw new Error(json.error ?? 'Unknown error');
534
+ }
535
+ const data = json.data;
536
+ if (cmd === 'PUSH' && data && typeof data === 'object') {
537
+ return { ok: true, id: data.id };
538
+ }
539
+ if ((cmd === 'PULL' || cmd === 'PULLB') && Array.isArray(data)) {
540
+ if (data.length === 0) {
541
+ return { ok: true, job: null };
542
+ }
543
+ return cmd === 'PULL'
544
+ ? { ok: true, job: data[0] }
545
+ : { ok: true, jobs: data };
546
+ }
547
+ if (cmd === 'STATS' && data && typeof data === 'object') {
548
+ return { ok: true, ...data };
549
+ }
550
+ if (cmd === 'METRICS' && data && typeof data === 'object') {
551
+ return { ok: true, ...data };
552
+ }
553
+ if (cmd === 'LISTQUEUES' && Array.isArray(data)) {
554
+ return { ok: true, queues: data };
555
+ }
556
+ return json;
557
+ }
558
+ catch (error) {
559
+ if (error instanceof Error && error.name === 'AbortError') {
560
+ throw new Error('HTTP request timeout');
561
+ }
562
+ throw error;
563
+ }
564
+ finally {
565
+ clearTimeout(timeoutId);
566
+ }
567
+ }
568
+ }
569
+ exports.FlashQConnection = FlashQConnection;
570
+ //# sourceMappingURL=connection.js.map