alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
396 lines (306 loc) • 10.7 kB
text/typescript
import { Alepha, z } from "alepha";
import { $logger } from "alepha/logger";
import { describe, expect, test, vi } from "vitest";
import {
$consumer,
$queue,
MemoryQueueProvider,
QueueProvider,
queueWorkerOptions,
WorkerProvider,
} from "../index.ts";
const payloadSchema = z.object({
id: z.text(),
count: z.integer(),
});
class TestWorkerProvider extends WorkerProvider {
public readonly log = $logger();
public workersRunning = 0;
public workerIntervals: Record<number, number> = {};
public abortController = new AbortController();
public waitForNextMessage(n: number): Promise<void> {
return super.waitForNextMessage(n);
}
}
describe("WorkerProvider", () => {
const createTestApp = async (
options: {
workerConcurrency?: number;
workerInterval?: number;
workerMaxInterval?: number;
} = {},
) => {
const app = Alepha.create();
app.store.mut(queueWorkerOptions, () => ({
concurrency: options.workerConcurrency ?? 1,
interval: options.workerInterval ?? 10,
maxInterval: options.workerMaxInterval ?? 1000,
}));
app.with({
provide: WorkerProvider,
use: TestWorkerProvider,
});
app.with({
provide: QueueProvider,
use: MemoryQueueProvider,
});
return app;
};
describe("Worker Lifecycle", () => {
test("should start workers when consumers are present", async () => {
class TestService {
queue = $queue({
name: "test",
schema: payloadSchema,
handler: async () => {
// Just a dummy handler for this test
},
});
}
const app = await createTestApp();
app.with(TestService);
const workerProvider = app.inject(TestWorkerProvider);
const logSpy = vi.spyOn(workerProvider.log, "debug");
await app.start();
expect(logSpy).toHaveBeenCalledWith("Starting worker n-0");
expect(workerProvider.workersRunning).toBe(1);
await app.stop();
expect(workerProvider.workersRunning).toBe(0);
});
test("should start multiple workers with concurrency", async () => {
class TestService {
queue = $queue({
name: "test",
schema: payloadSchema,
handler: async () => {},
});
}
const app = await createTestApp({ workerConcurrency: 3 });
app.with(TestService);
const workerProvider = app.inject(TestWorkerProvider);
const logSpy = vi.spyOn(workerProvider.log, "debug");
await app.start();
expect(logSpy).toHaveBeenCalledWith("Starting worker n-0");
expect(logSpy).toHaveBeenCalledWith("Starting worker n-1");
expect(logSpy).toHaveBeenCalledWith("Starting worker n-2");
expect(workerProvider.workersRunning).toBe(3);
await app.stop();
expect(workerProvider.workersRunning).toBe(0);
});
test("should not start workers when no consumers", async () => {
const app = await createTestApp();
const workerProvider = app.inject(TestWorkerProvider);
const logSpy = vi.spyOn(workerProvider.log, "debug");
await app.start();
expect(logSpy).not.toHaveBeenCalledWith(
expect.stringMatching(/Starting worker/),
);
expect(workerProvider.workersRunning).toBe(0);
await app.stop();
});
});
describe("WakeUp Functionality", () => {
test("should wake up workers and start missing ones", async () => {
class TestService {
queue = $queue({
name: "test",
schema: payloadSchema,
handler: async () => {},
});
}
const app = await createTestApp({ workerConcurrency: 2 });
app.with(TestService);
const workerProvider = app.inject(TestWorkerProvider);
const debugSpy = vi.spyOn(workerProvider.log, "debug");
await app.start();
expect(workerProvider.workersRunning).toBe(2);
// Simulate worker crash by manually decrementing counter
workerProvider.workersRunning = 1;
// Call wakeUp - should detect missing worker and restart it
workerProvider.wakeUp();
expect(debugSpy).toHaveBeenCalledWith("Waking up workers...");
expect(workerProvider.workersRunning).toBe(2);
await app.stop();
});
test("should create new AbortController on wakeUp", async () => {
class TestService {
queue = $queue({
name: "test",
schema: payloadSchema,
handler: async () => {},
});
}
const app = await createTestApp();
app.with(TestService);
const workerProvider = app.inject(TestWorkerProvider);
await app.start();
const oldController = workerProvider.abortController;
expect(oldController.signal.aborted).toBe(false);
workerProvider.wakeUp();
const newController = workerProvider.abortController;
expect(oldController.signal.aborted).toBe(true);
expect(newController.signal.aborted).toBe(false);
expect(newController).not.toBe(oldController);
await app.stop();
});
});
describe("Message Processing", () => {
test("should process messages correctly", async () => {
const messages: any[] = [];
class TestService {
queue = $queue({
schema: payloadSchema,
handler: async ({ payload }) => {
messages.push(payload);
},
});
}
const app = await createTestApp({ workerInterval: 5 });
app.with(TestService);
await app.start();
const testService = app.inject(TestService);
await testService.queue.push({ id: "msg1", count: 5 });
await testService.queue.push({ id: "msg2", count: 10 });
await expect
.poll(() => messages.length === 2, { timeout: 1000 })
.toBeTruthy();
expect(messages).toEqual([
{ id: "msg1", count: 5 },
{ id: "msg2", count: 10 },
]);
await app.stop();
});
test("should handle message processing errors gracefully", async () => {
class TestService {
queue = $queue({
name: "test",
schema: payloadSchema,
handler: async ({ payload }) => {
if (payload.id === "error") {
throw new Error("Processing error");
}
},
});
}
const app = await createTestApp();
app.with(TestService);
const workerProvider = app.inject(TestWorkerProvider);
const errorSpy = vi.spyOn(workerProvider.log, "error");
await app.start();
const testService = app.inject(TestService);
await testService.queue.push({ id: "error", count: 1 });
await expect
.poll(() => errorSpy.mock.calls.length > 0, { timeout: 500 })
.toBeTruthy();
// Worker should still be running after processing error
expect(workerProvider.workersRunning).toBe(1);
await app.stop();
});
test("should handle consumer with queue descriptor", async () => {
const messages: any[] = [];
class TestService {
queue = $queue({
name: "test",
schema: payloadSchema,
});
consumer = $consumer({
queue: this.queue,
handler: async ({ payload }) => {
messages.push(payload);
},
});
}
const app = await createTestApp();
app.with(TestService);
await app.start();
const testService = app.inject(TestService);
await testService.queue.push({ id: "consumer-test", count: 15 });
await expect
.poll(() => messages.length === 1, { timeout: 500 })
.toBeTruthy();
expect(messages).toEqual([{ id: "consumer-test", count: 15 }]);
await app.stop();
});
});
describe("Edge Cases", () => {
test("should handle malformed JSON messages", async () => {
class TestService {
queue = $queue({
name: "test",
schema: payloadSchema,
handler: async () => {},
});
}
const app = await createTestApp();
app.with(TestService);
const workerProvider = app.inject(TestWorkerProvider);
const queueProvider = app.inject(QueueProvider);
const errorSpy = vi.spyOn(workerProvider.log, "error");
await app.start();
// Push malformed JSON directly to the queue
await queueProvider.push("test", "invalid-json");
await expect
.poll(() => errorSpy.mock.calls.length > 0, { timeout: 500 })
.toBeTruthy();
expect(errorSpy).toHaveBeenCalledWith(
"Failed to process message",
expect.any(Error),
);
// Worker should still be running
expect(workerProvider.workersRunning).toBe(1);
await app.stop();
});
test("should handle schema validation errors", async () => {
class TestService {
queue = $queue({
name: "test",
schema: payloadSchema,
handler: async () => {},
});
}
const app = await createTestApp();
app.with(TestService);
const workerProvider = app.inject(TestWorkerProvider);
const queueProvider = app.inject(QueueProvider);
const errorSpy = vi.spyOn(workerProvider.log, "error");
await app.start();
// Push message with invalid schema
await queueProvider.push(
"test",
JSON.stringify({
payload: { id: 123, count: "invalid" }, // id should be string, count should be number
}),
);
await expect
.poll(() => errorSpy.mock.calls.length > 0, { timeout: 500 })
.toBeTruthy();
expect(errorSpy).toHaveBeenCalledWith(
"Failed to process message",
expect.any(Error),
);
// Worker should still be running
expect(workerProvider.workersRunning).toBe(1);
await app.stop();
});
test("should handle abort signal during wait", async () => {
class TestService {
queue = $queue({
name: "test",
schema: payloadSchema,
handler: async () => {},
});
}
const app = await createTestApp({ workerInterval: 5000 });
app.with(TestService);
const workerProvider = app.inject(TestWorkerProvider);
const warnSpy = vi.spyOn(workerProvider.log, "warn");
await app.start();
// Abort the controller to simulate abort during wait
workerProvider.abortController.abort();
// This should detect the abort and return early
await workerProvider.waitForNextMessage(0);
expect(warnSpy).toHaveBeenCalledWith("Worker n-0 aborted.");
await app.stop();
});
});
});