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.
- package/README.md +283 -0
- package/dist/client/advanced.d.ts +174 -0
- package/dist/client/advanced.d.ts.map +1 -0
- package/dist/client/advanced.js +248 -0
- package/dist/client/advanced.js.map +1 -0
- package/dist/client/connection.d.ts +103 -0
- package/dist/client/connection.d.ts.map +1 -0
- package/dist/client/connection.js +570 -0
- package/dist/client/connection.js.map +1 -0
- package/dist/client/core.d.ts +119 -0
- package/dist/client/core.d.ts.map +1 -0
- package/dist/client/core.js +257 -0
- package/dist/client/core.js.map +1 -0
- package/dist/client/cron.d.ts +59 -0
- package/dist/client/cron.d.ts.map +1 -0
- package/dist/client/cron.js +82 -0
- package/dist/client/cron.js.map +1 -0
- package/dist/client/dlq.d.ts +52 -0
- package/dist/client/dlq.d.ts.map +1 -0
- package/dist/client/dlq.js +73 -0
- package/dist/client/dlq.js.map +1 -0
- package/dist/client/flows.d.ts +49 -0
- package/dist/client/flows.d.ts.map +1 -0
- package/dist/client/flows.js +67 -0
- package/dist/client/flows.js.map +1 -0
- package/dist/client/index.d.ts +644 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +829 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/jobs.d.ts +183 -0
- package/dist/client/jobs.d.ts.map +1 -0
- package/dist/client/jobs.js +272 -0
- package/dist/client/jobs.js.map +1 -0
- package/dist/client/kv.d.ts +63 -0
- package/dist/client/kv.d.ts.map +1 -0
- package/dist/client/kv.js +131 -0
- package/dist/client/kv.js.map +1 -0
- package/dist/client/metrics.d.ts +34 -0
- package/dist/client/metrics.d.ts.map +1 -0
- package/dist/client/metrics.js +49 -0
- package/dist/client/metrics.js.map +1 -0
- package/dist/client/pubsub.d.ts +42 -0
- package/dist/client/pubsub.d.ts.map +1 -0
- package/dist/client/pubsub.js +92 -0
- package/dist/client/pubsub.js.map +1 -0
- package/dist/client/queue.d.ts +111 -0
- package/dist/client/queue.d.ts.map +1 -0
- package/dist/client/queue.js +160 -0
- package/dist/client/queue.js.map +1 -0
- package/dist/client/types.d.ts +23 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +3 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client.d.ts +17 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +23 -0
- package/dist/client.js.map +1 -0
- package/dist/events.d.ts +184 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +340 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/queue.d.ts +104 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +139 -0
- package/dist/queue.js.map +1 -0
- package/dist/types.d.ts +185 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/worker.d.ts +88 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +296 -0
- package/dist/worker.js.map +1 -0
- 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
|