UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

459 lines (377 loc) 12.7 kB
import { $hook, Alepha, z } from "alepha"; import { DateTimeProvider } from "alepha/datetime"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { $batch, AlephaBatch } from "../index.ts"; /** * Tests for startup buffering behavior. * * Requirements: * - During startup (before "ready" hook), items should be buffered in memory * - No size checks should occur during startup * - No timeouts should be created during startup * - After "ready" hook, all buffered items should be processed correctly * - Items pushed after "ready" should behave normally */ describe("Batch startup buffering", () => { let alepha: Alepha; let time: DateTimeProvider; beforeEach(() => { alepha = Alepha.create().with(AlephaBatch); time = alepha.inject(DateTimeProvider); }); afterEach(async () => { await alepha.stop(); }); test("should buffer items during startup without size checks", async () => { const mockHandler = vi.fn(async (items: string[]) => { return `processed ${items.length} items`; }); class TestApp { batcher = $batch({ schema: z.text(), maxSize: 10, // Would normally trigger flush at 10 items maxDuration: [1, "second"], handler: mockHandler, }); // Push items during start hook (before ready) onStart = $hook({ on: "start", handler: async () => { // Push 15 items during startup (exceeds maxSize of 10) for (let i = 0; i < 15; i++) { await this.batcher.push(`startup-item-${i}`); } }, }); } const app = alepha.inject(TestApp); // Handler should not be called during startup expect(mockHandler).not.toHaveBeenCalled(); await alepha.start(); // After start completes (which includes "ready" hook), handler should be called await vi.waitFor(() => { expect(mockHandler).toHaveBeenCalled(); }); // Should have flushed once with 10 items (maxSize) // and started a timeout for the remaining 5 items expect(mockHandler).toHaveBeenCalledTimes(1); expect(mockHandler).toHaveBeenNthCalledWith( 1, expect.arrayContaining([ "startup-item-0", "startup-item-1", "startup-item-2", "startup-item-3", "startup-item-4", "startup-item-5", "startup-item-6", "startup-item-7", "startup-item-8", "startup-item-9", ]), ); // Wait for timeout to flush remaining items await time.travel([1.1, "seconds"]); await vi.waitFor(() => { expect(mockHandler).toHaveBeenCalledTimes(2); }); expect(mockHandler).toHaveBeenNthCalledWith( 2, expect.arrayContaining([ "startup-item-10", "startup-item-11", "startup-item-12", "startup-item-13", "startup-item-14", ]), ); }); test("should not create timeouts during startup", async () => { const mockHandler = vi.fn(async (items: string[]) => { return "processed"; }); class TestApp { batcher = $batch({ schema: z.text(), maxSize: 100, maxDuration: [100, "milliseconds"], // Short timeout handler: mockHandler, }); onStart = $hook({ on: "start", handler: async () => { // Push 5 items during startup await this.batcher.push("item-1"); await this.batcher.push("item-2"); await this.batcher.push("item-3"); await this.batcher.push("item-4"); await this.batcher.push("item-5"); }, }); } const app = alepha.inject(TestApp); // Travel time forward before starting (should have no effect) await time.travel([200, "milliseconds"]); // Handler should still not be called expect(mockHandler).not.toHaveBeenCalled(); await alepha.start(); // After ready, timeout should be created // Wait for timeout to trigger await time.travel([150, "milliseconds"]); await vi.waitFor(() => { expect(mockHandler).toHaveBeenCalledTimes(1); }); expect(mockHandler).toHaveBeenCalledWith([ "item-1", "item-2", "item-3", "item-4", "item-5", ]); }); test("should handle 1000+ items pushed during startup", async () => { const mockHandler = vi.fn(async (items: string[]) => { return `batch of ${items.length}`; }); class TestApp { batcher = $batch({ schema: z.text(), maxSize: 100, maxDuration: [1, "second"], handler: mockHandler, }); onStart = $hook({ on: "start", handler: async () => { // Push 1000 items during startup const pushPromises: Promise<string>[] = []; for (let i = 0; i < 1000; i++) { pushPromises.push(this.batcher.push(`item-${i}`)); } await Promise.all(pushPromises); }, }); } const app = alepha.inject(TestApp); // Handler should not be called during startup expect(mockHandler).not.toHaveBeenCalled(); await alepha.start(); // After ready, should flush batches of 100 items each await vi.waitFor( () => { // Should have 10 batches (1000 / 100) expect(mockHandler).toHaveBeenCalledTimes(10); }, { timeout: 2000 }, ); // Each call should have 100 items for (let i = 0; i < 10; i++) { const call = mockHandler.mock.calls[i]; expect(call[0]).toHaveLength(100); } }); test("should handle partitioned items during startup", async () => { const mockHandler = vi.fn(async (items: any[]) => { return "processed"; }); class TestApp { batcher = $batch({ schema: z.object({ partition: z.text(), value: z.text() }), maxSize: 5, partitionBy: (item) => item.partition, handler: mockHandler, }); onStart = $hook({ on: "start", handler: async () => { // Push items to different partitions await this.batcher.push({ partition: "A", value: "a1" }); await this.batcher.push({ partition: "A", value: "a2" }); await this.batcher.push({ partition: "B", value: "b1" }); await this.batcher.push({ partition: "B", value: "b2" }); await this.batcher.push({ partition: "A", value: "a3" }); await this.batcher.push({ partition: "A", value: "a4" }); await this.batcher.push({ partition: "A", value: "a5" }); // Partition A now has 5 items (maxSize) await this.batcher.push({ partition: "A", value: "a6" }); // Partition A now has 6 items (exceeds maxSize during startup - should NOT flush) }, }); } const app = alepha.inject(TestApp); // No flushes during startup expect(mockHandler).not.toHaveBeenCalled(); await alepha.start(); // After ready, partition A should flush immediately (6 > 5) // It will flush 5 items first (maxSize), leaving 1 item await vi.waitFor(() => { expect(mockHandler).toHaveBeenCalledTimes(1); }); // Should flush partition A with 5 items (first batch of maxSize) const firstCall = mockHandler.mock.calls[0][0]; expect(firstCall).toHaveLength(5); expect(firstCall).toEqual([ { partition: "A", value: "a1" }, { partition: "A", value: "a2" }, { partition: "A", value: "a3" }, { partition: "A", value: "a4" }, { partition: "A", value: "a5" }, ]); // Partition A has 1 remaining item and partition B has 2 items // Wait for timeout to flush them await time.travel([1.1, "seconds"]); await vi.waitFor(() => { expect(mockHandler).toHaveBeenCalledTimes(3); }); // Second call should be the remaining item from partition A const secondCall = mockHandler.mock.calls[1][0]; expect(secondCall).toHaveLength(1); expect(secondCall).toEqual([{ partition: "A", value: "a6" }]); // Third call should be partition B const thirdCall = mockHandler.mock.calls[2][0]; expect(thirdCall).toHaveLength(2); expect(thirdCall).toEqual([ { partition: "B", value: "b1" }, { partition: "B", value: "b2" }, ]); }); test("should work correctly when items are pushed both during and after startup", async () => { const mockHandler = vi.fn(async (items: string[]) => { return "processed"; }); class TestApp { batcher = $batch({ schema: z.text(), maxSize: 3, maxDuration: [500, "milliseconds"], handler: mockHandler, }); onStart = $hook({ on: "start", handler: async () => { await this.batcher.push("startup-1"); await this.batcher.push("startup-2"); }, }); } const app = alepha.inject(TestApp); // No handler calls yet expect(mockHandler).not.toHaveBeenCalled(); await alepha.start(); // After ready, timeout starts for the 2 startup items // Push another item (should trigger flush at maxSize=3) await app.batcher.push("runtime-1"); await vi.waitFor(() => { expect(mockHandler).toHaveBeenCalledTimes(1); }); expect(mockHandler).toHaveBeenCalledWith([ "startup-1", "startup-2", "runtime-1", ]); // Push more items after flush await app.batcher.push("runtime-2"); await app.batcher.push("runtime-3"); // Wait for timeout await time.travel([600, "milliseconds"]); await vi.waitFor(() => { expect(mockHandler).toHaveBeenCalledTimes(2); }); expect(mockHandler).toHaveBeenNthCalledWith(2, ["runtime-2", "runtime-3"]); }); test("should handle wait() for items pushed during startup", async () => { const mockHandler = vi.fn(async (items: string[]) => { return items.map((item) => `processed-${item}`); }); class TestApp { batcher = $batch({ schema: z.text(), maxSize: 10, handler: mockHandler, }); startupIds: string[] = []; onStart = $hook({ on: "start", handler: async () => { this.startupIds.push(await this.batcher.push("item-1")); this.startupIds.push(await this.batcher.push("item-2")); this.startupIds.push(await this.batcher.push("item-3")); }, }); } const app = alepha.inject(TestApp); await alepha.start(); // Wait for all startup items to be processed const results = await Promise.all( app.startupIds.map((id) => app.batcher.wait(id)), ); expect(results).toEqual([ ["processed-item-1", "processed-item-2", "processed-item-3"], ["processed-item-1", "processed-item-2", "processed-item-3"], ["processed-item-1", "processed-item-2", "processed-item-3"], ]); }); test("should immediately flush startup items that exceed maxSize on ready", async () => { const mockHandler = vi.fn(async (items: string[]) => { return "processed"; }); class TestApp { batcher = $batch({ schema: z.text(), maxSize: 5, maxDuration: [10, "seconds"], // Long timeout so it doesn't interfere handler: mockHandler, }); onStart = $hook({ on: "start", handler: async () => { // Push exactly maxSize items for (let i = 0; i < 5; i++) { await this.batcher.push(`item-${i}`); } }, }); } const app = alepha.inject(TestApp); await alepha.start(); // Should flush immediately on ready (5 >= maxSize) await vi.waitFor(() => { expect(mockHandler).toHaveBeenCalledTimes(1); }); expect(mockHandler).toHaveBeenCalledWith([ "item-0", "item-1", "item-2", "item-3", "item-4", ]); }); test("should handle status() for buffered items", async () => { const mockHandler = vi.fn(async (items: string[]) => { return "success"; }); class TestApp { batcher = $batch({ schema: z.text(), maxSize: 10, handler: mockHandler, }); itemId?: string; onStart = $hook({ on: "start", handler: async () => { this.itemId = await this.batcher.push("test-item"); }, }); } const app = alepha.inject(TestApp); await alepha.start(); // Item should be pending or processing const status1 = app.batcher.status(app.itemId!); expect(status1?.status).toMatch(/pending|processing|completed/); // Wait for processing await app.batcher.wait(app.itemId!); const status2 = app.batcher.status(app.itemId!); expect(status2?.status).toBe("completed"); }); });