qrusty-client 0.10.0 → 0.12.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/live_test.js +362 -0
- package/package.json +3 -2
package/live_test.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Qrusty Node.js Live Client Tests
|
|
4
|
+
*
|
|
5
|
+
* Starts a real qrusty server and exercises QrustyClient (HTTP) and
|
|
6
|
+
* WsSession (WebSocket) against it, including cross-mode scenarios.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node integrations/nodeclient/live_test.js [--binary PATH] [--port PORT]
|
|
10
|
+
*
|
|
11
|
+
* Exit codes: 0 pass, 1 failure, 2 setup failure.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const assert = require("assert");
|
|
15
|
+
const { spawn, execSync } = require("child_process");
|
|
16
|
+
const http = require("http");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const os = require("os");
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const { QrustyClient, WsSession } = require("./index");
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Defaults
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..");
|
|
26
|
+
const DEFAULT_BINARY = path.join(REPO_ROOT, "target", "debug", "qrusty");
|
|
27
|
+
const DEFAULT_PORT = 17787;
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// CLI args
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
function parseArgs() {
|
|
33
|
+
const args = { binary: DEFAULT_BINARY, port: DEFAULT_PORT };
|
|
34
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
35
|
+
if (process.argv[i] === "--binary" && process.argv[i + 1]) {
|
|
36
|
+
args.binary = process.argv[++i];
|
|
37
|
+
}
|
|
38
|
+
if (process.argv[i] === "--port" && process.argv[i + 1]) {
|
|
39
|
+
args.port = parseInt(process.argv[++i], 10);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Resolve target-specific binary path
|
|
43
|
+
if (!fs.existsSync(args.binary)) {
|
|
44
|
+
try {
|
|
45
|
+
const hostTarget = execSync("rustc -vV", { encoding: "utf8" })
|
|
46
|
+
.split("\n")
|
|
47
|
+
.find((l) => l.startsWith("host:"))
|
|
48
|
+
?.split(":")[1]
|
|
49
|
+
?.trim();
|
|
50
|
+
if (hostTarget) {
|
|
51
|
+
const alt = path.join(
|
|
52
|
+
REPO_ROOT,
|
|
53
|
+
"target",
|
|
54
|
+
hostTarget,
|
|
55
|
+
"debug",
|
|
56
|
+
"qrusty",
|
|
57
|
+
);
|
|
58
|
+
if (fs.existsSync(alt)) args.binary = alt;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// ignore
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Server lifecycle
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
function startServer(binary, port, dataDir) {
|
|
71
|
+
const env = {
|
|
72
|
+
...process.env,
|
|
73
|
+
BIND_ADDR: `0.0.0.0:${port}`,
|
|
74
|
+
DATA_PATH: dataDir,
|
|
75
|
+
};
|
|
76
|
+
return spawn(binary, [], {
|
|
77
|
+
env,
|
|
78
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function waitForHealth(baseUrl, timeoutMs = 15000) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const deadline = Date.now() + timeoutMs;
|
|
85
|
+
const poll = () => {
|
|
86
|
+
if (Date.now() > deadline) {
|
|
87
|
+
return reject(new Error(`Server at ${baseUrl} not healthy in time`));
|
|
88
|
+
}
|
|
89
|
+
http
|
|
90
|
+
.get(`${baseUrl}/health`, (res) => {
|
|
91
|
+
if (res.statusCode === 200) return resolve();
|
|
92
|
+
setTimeout(poll, 250);
|
|
93
|
+
})
|
|
94
|
+
.on("error", () => setTimeout(poll, 250));
|
|
95
|
+
};
|
|
96
|
+
poll();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function stopServer(proc) {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
proc.on("close", resolve);
|
|
103
|
+
proc.kill("SIGTERM");
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
try {
|
|
106
|
+
proc.kill("SIGKILL");
|
|
107
|
+
} catch {
|
|
108
|
+
// already dead
|
|
109
|
+
}
|
|
110
|
+
}, 5000);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Test runner
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
let passed = 0;
|
|
118
|
+
let failed = 0;
|
|
119
|
+
let queueCounter = 0;
|
|
120
|
+
|
|
121
|
+
function uniqueQueue(prefix = "nodeq") {
|
|
122
|
+
return `${prefix}-${++queueCounter}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function runTest(name, fn) {
|
|
126
|
+
try {
|
|
127
|
+
await fn();
|
|
128
|
+
console.log(` PASS ${name}`);
|
|
129
|
+
passed++;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.log(` FAIL ${name}`);
|
|
132
|
+
console.log(` ${err.message}`);
|
|
133
|
+
failed++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Helpers
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
function sleep(ms) {
|
|
141
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function receiveOne(session, queue, timeoutMs = 5000) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const timer = setTimeout(
|
|
147
|
+
() => reject(new Error(`timeout waiting for delivery on ${queue}`)),
|
|
148
|
+
timeoutMs,
|
|
149
|
+
);
|
|
150
|
+
const iter = session.subscribe(queue)[Symbol.asyncIterator]();
|
|
151
|
+
iter.next().then((result) => {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
resolve(result.value);
|
|
154
|
+
}, reject);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Main
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
async function main() {
|
|
162
|
+
const args = parseArgs();
|
|
163
|
+
if (!fs.existsSync(args.binary)) {
|
|
164
|
+
console.error(
|
|
165
|
+
`ERROR: binary not found at ${args.binary}. Run 'cargo build' first.`,
|
|
166
|
+
);
|
|
167
|
+
process.exit(2);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "qrusty-node-live-"));
|
|
171
|
+
const baseUrl = `http://127.0.0.1:${args.port}`;
|
|
172
|
+
const wsUrl = `ws://127.0.0.1:${args.port}`;
|
|
173
|
+
const proc = startServer(args.binary, args.port, dataDir);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
await waitForHealth(baseUrl);
|
|
177
|
+
console.log(`Server started at ${baseUrl}\n`);
|
|
178
|
+
|
|
179
|
+
const client = new QrustyClient(baseUrl);
|
|
180
|
+
|
|
181
|
+
// ======================= HTTP Tests ===========================
|
|
182
|
+
console.log("HTTP Client Tests:");
|
|
183
|
+
|
|
184
|
+
await runTest("health", async () => {
|
|
185
|
+
const h = await client.health();
|
|
186
|
+
assert.ok(h.includes("OK"));
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await runTest("createQueue and listQueues", async () => {
|
|
190
|
+
const q = uniqueQueue();
|
|
191
|
+
await client.createQueue(q);
|
|
192
|
+
// /queues only lists queues with messages, so publish one first
|
|
193
|
+
await client.publish(q, 1, "test");
|
|
194
|
+
const list = await client.listQueues();
|
|
195
|
+
assert.ok(list.includes(q));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await runTest("publish returns id", async () => {
|
|
199
|
+
const q = uniqueQueue();
|
|
200
|
+
await client.createQueue(q);
|
|
201
|
+
const result = await client.publish(q, 100, { x: 1 });
|
|
202
|
+
assert.ok(result.id);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await runTest("consume delivers highest priority", async () => {
|
|
206
|
+
const q = uniqueQueue();
|
|
207
|
+
await client.createQueue(q);
|
|
208
|
+
await client.publish(q, 10, "low");
|
|
209
|
+
await client.publish(q, 50, "high");
|
|
210
|
+
await client.publish(q, 30, "mid");
|
|
211
|
+
const msg = await client.consume(q, "test");
|
|
212
|
+
// ConsumeResponse only has {id, payload, retry_count} — verify via payload
|
|
213
|
+
assert.strictEqual(msg.payload, "high");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await runTest("consume empty returns null", async () => {
|
|
217
|
+
const q = uniqueQueue();
|
|
218
|
+
await client.createQueue(q);
|
|
219
|
+
const msg = await client.consume(q, "test");
|
|
220
|
+
assert.strictEqual(msg, null);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await runTest("ack removes message", async () => {
|
|
224
|
+
const q = uniqueQueue();
|
|
225
|
+
await client.createQueue(q);
|
|
226
|
+
await client.publish(q, 1, "msg");
|
|
227
|
+
const msg = await client.consume(q, "test");
|
|
228
|
+
await client.ack(q, msg.id, "test");
|
|
229
|
+
const stats = await client.queueStats(q);
|
|
230
|
+
assert.strictEqual(stats.locked, 0);
|
|
231
|
+
assert.strictEqual(stats.available, 0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await runTest("nack requeues message", async () => {
|
|
235
|
+
const q = uniqueQueue();
|
|
236
|
+
await client.createQueue(q);
|
|
237
|
+
await client.publish(q, 1, "msg");
|
|
238
|
+
const msg = await client.consume(q, "test");
|
|
239
|
+
await client.nack(q, msg.id, "test");
|
|
240
|
+
const stats = await client.queueStats(q);
|
|
241
|
+
assert.strictEqual(stats.locked, 0);
|
|
242
|
+
assert.strictEqual(stats.available, 1);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await runTest("purge queue", async () => {
|
|
246
|
+
const q = uniqueQueue();
|
|
247
|
+
await client.createQueue(q);
|
|
248
|
+
await client.publish(q, 1, "a");
|
|
249
|
+
await client.publish(q, 2, "b");
|
|
250
|
+
await client.purge(q);
|
|
251
|
+
const stats = await client.queueStats(q);
|
|
252
|
+
assert.strictEqual(stats.available, 0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await runTest("delete queue", async () => {
|
|
256
|
+
const q = uniqueQueue();
|
|
257
|
+
await client.createQueue(q);
|
|
258
|
+
await client.deleteQueue(q);
|
|
259
|
+
const list = await client.listQueues();
|
|
260
|
+
assert.ok(!list.includes(q));
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ======================= WS Tests ===========================
|
|
264
|
+
console.log("\nWebSocket Client Tests:");
|
|
265
|
+
|
|
266
|
+
await runTest("ws publish and subscribe", async () => {
|
|
267
|
+
const q = uniqueQueue("wsq");
|
|
268
|
+
await client.createQueue(q);
|
|
269
|
+
|
|
270
|
+
const session = new WsSession(wsUrl);
|
|
271
|
+
await session.connect();
|
|
272
|
+
try {
|
|
273
|
+
const id = await session.publish(q, "hello-ws", 42);
|
|
274
|
+
assert.ok(id);
|
|
275
|
+
|
|
276
|
+
// Small delay to let subscribe frame reach server before delivery
|
|
277
|
+
const receivePromise = receiveOne(session, q);
|
|
278
|
+
await sleep(100);
|
|
279
|
+
const msg = await receivePromise;
|
|
280
|
+
assert.strictEqual(msg.queue, q);
|
|
281
|
+
assert.strictEqual(msg.payload, "hello-ws");
|
|
282
|
+
assert.strictEqual(msg.priority, 42);
|
|
283
|
+
} finally {
|
|
284
|
+
await session.close();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await runTest("ws ack clears locked", async () => {
|
|
289
|
+
const q = uniqueQueue("wsq");
|
|
290
|
+
await client.createQueue(q);
|
|
291
|
+
|
|
292
|
+
const session = new WsSession(wsUrl);
|
|
293
|
+
await session.connect();
|
|
294
|
+
try {
|
|
295
|
+
await session.publish(q, "ack-test", 1);
|
|
296
|
+
const receivePromise = receiveOne(session, q);
|
|
297
|
+
await sleep(100);
|
|
298
|
+
const msg = await receivePromise;
|
|
299
|
+
await session.ack(msg.queue, msg.id);
|
|
300
|
+
} finally {
|
|
301
|
+
await session.close();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const stats = await client.queueStats(q);
|
|
305
|
+
assert.strictEqual(stats.locked, 0);
|
|
306
|
+
assert.strictEqual(stats.available, 0);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ======================= Cross-Mode Tests ===========================
|
|
310
|
+
console.log("\nCross-Mode Tests:");
|
|
311
|
+
|
|
312
|
+
await runTest("HTTP publish -> WS subscribe", async () => {
|
|
313
|
+
const q = uniqueQueue("cross");
|
|
314
|
+
await client.createQueue(q);
|
|
315
|
+
|
|
316
|
+
const session = new WsSession(wsUrl);
|
|
317
|
+
await session.connect();
|
|
318
|
+
try {
|
|
319
|
+
const receivePromise = receiveOne(session, q);
|
|
320
|
+
await sleep(200); // Let subscribe settle
|
|
321
|
+
await client.publish(q, 77, "from-http");
|
|
322
|
+
const msg = await receivePromise;
|
|
323
|
+
assert.strictEqual(msg.queue, q);
|
|
324
|
+
assert.strictEqual(msg.priority, 77);
|
|
325
|
+
} finally {
|
|
326
|
+
await session.close();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
await runTest("WS publish -> HTTP consume", async () => {
|
|
331
|
+
const q = uniqueQueue("cross");
|
|
332
|
+
await client.createQueue(q);
|
|
333
|
+
|
|
334
|
+
const session = new WsSession(wsUrl);
|
|
335
|
+
await session.connect();
|
|
336
|
+
try {
|
|
337
|
+
await session.publish(q, "from-ws", 88);
|
|
338
|
+
} finally {
|
|
339
|
+
await session.close();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const msg = await client.consume(q, "test");
|
|
343
|
+
assert.ok(msg);
|
|
344
|
+
assert.strictEqual(msg.payload, "from-ws");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ======================= Summary ===========================
|
|
348
|
+
console.log(`\n${"=".repeat(50)}`);
|
|
349
|
+
console.log(`RESULTS: ${passed} passed, ${failed} failed`);
|
|
350
|
+
console.log("=".repeat(50));
|
|
351
|
+
|
|
352
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
353
|
+
} finally {
|
|
354
|
+
await stopServer(proc);
|
|
355
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
main().catch((err) => {
|
|
360
|
+
console.error(`Setup error: ${err.message}`);
|
|
361
|
+
process.exit(2);
|
|
362
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qrusty-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Node.js client for the qrusty priority queue server API.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"author": "Gordon Greene <greeng3@obscure-reference.com>",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"docs": "jsdoc -c jsdoc.json"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"axios": "^1.13.5"
|
|
13
|
+
"axios": "^1.13.5",
|
|
14
|
+
"ws": "^8.19.0"
|
|
14
15
|
},
|
|
15
16
|
"devDependencies": {
|
|
16
17
|
"jest": "30.2.0",
|