UNPKG

alepha

Version:

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

316 lines (270 loc) 7.96 kB
import { Alepha, AlephaError, z } from "alepha"; import { LinkProvider, ServerLinksProvider } from "alepha/server/links"; import { describe, test } from "vitest"; import { $sse } from "../index.ts"; describe("$sse", () => { test("should stream events via run()", async ({ expect }) => { class Api { events = $sse({ schema: { data: z.object({ type: z.text(), message: z.text(), }), }, handler: async ({ emit, close }) => { emit({ type: "greeting", message: "hello" }); emit({ type: "greeting", message: "world" }); close(); }, }); } const alepha = Alepha.create(); const app = alepha.inject(Api); await alepha.start(); const events: Array<{ type: string; message: string }> = []; for await (const event of app.events.run()) { events.push(event); } expect(events).toStrictEqual([ { type: "greeting", message: "hello" }, { type: "greeting", message: "world" }, ]); }); test("should have correct name and path", async ({ expect }) => { class Api { notifications = $sse({ schema: { data: z.object({ id: z.number(), }), }, handler: async ({ close }) => { close(); }, }); } const alepha = Alepha.create(); const app = alepha.inject(Api); await alepha.start(); expect(app.notifications.name).toBe("notifications"); expect(app.notifications.path).toBe("/notifications"); expect(app.notifications.method).toBe("POST"); expect(app.notifications.prefix).toBe("/api"); }); test("should use custom path when provided", async ({ expect }) => { class Api { feed = $sse({ path: "/custom/feed", schema: { data: z.object({ value: z.text(), }), }, handler: async ({ close }) => { close(); }, }); } const alepha = Alepha.create(); const app = alepha.inject(Api); await alepha.start(); expect(app.feed.path).toBe("/custom/feed"); }); test("should append params to path", async ({ expect }) => { class Api { stream = $sse({ schema: { params: z.object({ id: z.text(), }), data: z.object({ value: z.text(), }), }, handler: async ({ params, emit, close }) => { emit({ value: `item-${params.id}` }); close(); }, }); } const alepha = Alepha.create(); const app = alepha.inject(Api); await alepha.start(); expect(app.stream.path).toBe("/stream/:id"); const events: Array<{ value: string }> = []; for await (const event of app.stream.run({ params: { id: "42" } })) { events.push(event); } expect(events).toStrictEqual([{ value: "item-42" }]); }); test("should stream via fetch() (HTTP)", async ({ expect }) => { class Api { events = $sse({ schema: { data: z.object({ seq: z.number(), }), }, handler: async ({ emit, close }) => { emit({ seq: 1 }); emit({ seq: 2 }); emit({ seq: 3 }); close(); }, }); } const alepha = Alepha.create(); const app = alepha.inject(Api); await alepha.start(); const res = await app.events.fetch(); expect(res.status).toBe(200); expect(res.headers.get("content-type")).toBe("text/event-stream"); const events: Array<{ seq: number }> = []; for await (const event of res) { events.push(event); } expect(events).toStrictEqual([{ seq: 1 }, { seq: 2 }, { seq: 3 }]); }); test("should propagate handler errors through the stream", async ({ expect, }) => { class Api { failing = $sse({ schema: { data: z.object({ value: z.text(), }), }, handler: async ({ emit }) => { emit({ value: "before-error" }); throw new AlephaError("handler failed"); }, }); } const alepha = Alepha.create(); const app = alepha.inject(Api); await alepha.start(); const events: Array<{ value: string }> = []; await expect(async () => { for await (const event of app.failing.run()) { events.push(event); } }).rejects.toThrowError("handler failed"); expect(events).toStrictEqual([{ value: "before-error" }]); }); test("should auto-close stream when handler finishes", async ({ expect }) => { class Api { autoClose = $sse({ schema: { data: z.object({ n: z.number(), }), }, handler: async ({ emit }) => { emit({ n: 1 }); emit({ n: 2 }); // no explicit close() call }, }); } const alepha = Alepha.create(); const app = alepha.inject(Api); await alepha.start(); const events: Array<{ n: number }> = []; for await (const event of app.autoClose.run()) { events.push(event); } expect(events).toStrictEqual([{ n: 1 }, { n: 2 }]); }); test("should be callable as a function", async ({ expect }) => { class Api { ticker = $sse({ schema: { data: z.object({ tick: z.number(), }), }, handler: async ({ emit, close }) => { emit({ tick: 1 }); close(); }, }); } const alepha = Alepha.create(); const app = alepha.inject(Api); await alepha.start(); const events: Array<{ tick: number }> = []; for await (const event of app.ticker()) { events.push(event); } expect(events).toStrictEqual([{ tick: 1 }]); }); test("should not be exposed when disabled", async ({ expect }) => { class Api { disabled = $sse({ disabled: true, schema: { data: z.object({ value: z.text(), }), }, handler: async ({ close }) => { close(); }, }); } const alepha = Alepha.create(); const app = alepha.inject(Api); await alepha.start(); expect(() => app.disabled.run()).toThrowError( "SSE endpoint 'disabled' is disabled.", ); }); test("should work via $client proxy", async ({ expect }) => { class Api { chat = $sse({ schema: { body: z.object({ prompt: z.text() }), data: z.object({ token: z.text() }), }, handler: async ({ body, emit }) => { emit({ token: "Hello" }); emit({ token: body.prompt }); }, }); } const alepha = Alepha.create().with(Api).with(ServerLinksProvider); await alepha.start(); const linkProvider = alepha.inject(LinkProvider); const client = linkProvider.client<Api>(); const stream = await client.chat({ body: { prompt: "world" } }); const events: Array<{ token: string }> = []; for await (const event of stream) { events.push(event); } expect(events).toStrictEqual([{ token: "Hello" }, { token: "world" }]); }); test("should register as a link", async ({ expect }) => { class Api { chat = $sse({ schema: { body: z.object({ prompt: z.text() }), data: z.object({ token: z.text() }), }, handler: async ({ body, emit }) => { emit({ token: body.prompt }); }, }); } const alepha = Alepha.create().with(Api).with(ServerLinksProvider); await alepha.start(); const linkProvider = alepha.inject(LinkProvider); const links = linkProvider.getServerLinks(); const chatLink = links.find((l) => l.name === "chat"); expect(chatLink).toBeDefined(); expect(chatLink!.kind).toBe("sse"); expect(chatLink!.method).toBe("POST"); expect(chatLink!.path).toBe("/chat"); }); });