import { EventBus } from "@suaveplan/events"; import { afterEach, describe, expect, it } from "@suaveplan/testing/runner"; import { EventsProxyError } from "../error-codes/error-codes.js"; import { createProxyPair, waitFor } from "../testing/testing.js"; type Map = { ping: number; user: { id: string } }; const teardowns: Array<() => Promise> = []; afterEach(async () => { while (teardowns.length > 0) { const t = teardowns.pop(); if (t !== undefined) await t(); } }); function fresh(highWaterMark?: number) { const pair = createProxyPair({ ...(highWaterMark !== undefined ? { highWaterMark } : {}), }); teardowns.push(pair.tearDown); return pair; } describe("main-side proxy server", () => { it("delivers a main-side emit to a worker-side listener", { timeout: 10000, }, async () => { const { bus, client } = fresh(); const seen: number[] = []; client.on("ping", (n) => { seen.push(n); }); // Allow the streaming subscription to fully establish before // the first emit, so we observe in-order delivery. await waitFor(() => true, 100); await new Promise((r) => setTimeout(r, 50)); bus.emit("ping", 1); bus.emit("ping", 2); bus.emit("ping", 3); await waitFor(() => seen.length === 3, 2000); expect(seen).toStrictEqual([1, 2, 3]); }); it("supports once() with single delivery", { timeout: 10000 }, async () => { const { bus, client } = fresh(); const seen: number[] = []; client.once("ping", (n) => { seen.push(n); }); await new Promise((r) => setTimeout(r, 100)); bus.emit("ping", 10); bus.emit("ping", 20); await waitFor(() => seen.length >= 1, 2000); await new Promise((r) => setTimeout(r, 50)); expect(seen).toStrictEqual([10]); }); it("off() cancels delivery", { timeout: 10000 }, async () => { const { bus, client } = fresh(); const seen: number[] = []; const listener = (n: number): void => { seen.push(n); }; client.on("ping", listener); await new Promise((r) => setTimeout(r, 100)); bus.emit("ping", 1); await waitFor(() => seen.length === 1, 2000); client.off("ping", listener); await new Promise((r) => setTimeout(r, 50)); bus.emit("ping", 2); await new Promise((r) => setTimeout(r, 100)); expect(seen).toStrictEqual([1]); }); it("worker-side emit reaches main-side listener (fire-and-forget)", { timeout: 5000, }, async () => { const { bus, client } = fresh(); const seen: number[] = []; bus.on("ping", (n) => { seen.push(n); }); const result = client.emit("ping", 7); // emit returns synthetic zeros immediately. expect(result).toStrictEqual({ delivered: 0, errors: [] }); await waitFor(() => seen.length === 1, 2000); expect(seen).toStrictEqual([7]); }); it("worker-side emitAsync reports delivered count", { timeout: 5000, }, async () => { const { bus, client } = fresh(); bus.on("ping", () => undefined); bus.on("ping", () => undefined); const result = await client.emitAsync("ping", 1); expect(result.delivered).toBe(2); expect(result.errorCount).toBe(0); }); it("rejects non-cloneable payloads with UNSERIALISABLE_PAYLOAD", { timeout: 5000, }, async () => { const { client } = fresh(); expect(() => client.emit("user" as never, { id: "x", fn: () => 1 } as never) ).toThrow(EventsProxyError); }); it("emit rejects non-cloneable inputs main-side via emitAsync", { timeout: 5000, }, async () => { const { client } = fresh(); await expect( client.emitAsync("user" as never, { id: "x", fn: () => 1 } as never) ).rejects.toBeInstanceOf(EventsProxyError); }); it("dispose tears down server", { timeout: 5000 }, () => { const { server } = fresh(); server.dispose(); expect(server.activeSubscriptionCount).toBe(0); }); it("activeSubscriptionCount reflects open subscriptions", { timeout: 5000, }, async () => { const { client, server } = fresh(); client.on("ping", () => undefined); client.on("user", () => undefined); await waitFor(() => server.activeSubscriptionCount === 2, 2000); expect(server.activeSubscriptionCount).toBe(2); }); it("backpressure: subscription terminates when high-water exceeded", { timeout: 10000, }, async () => { const { bus, client } = fresh(2); const received: number[] = []; const errors: Error[] = []; client.on("ping", (v: unknown) => { if (v instanceof Error) errors.push(v); else received.push(v as number); }); await new Promise((r) => setTimeout(r, 100)); // Burst many emits — backpressure should fire because the // worker-side queue (inside streamRequest) is small. for (let i = 0; i < 200; i++) bus.emit("ping", i); await new Promise((r) => setTimeout(r, 300)); // Either we receive a bounded subset OR an error is surfaced. expect(received.length + errors.length).toBeGreaterThan(0); }); it("typed payloads survive the round-trip", { timeout: 5000 }, async () => { const { bus, client } = fresh(); const seen: Array<{ id: string }> = []; client.on("user", (u) => { seen.push(u); }); await new Promise((r) => setTimeout(r, 100)); bus.emit("user", { id: "u_1" }); bus.emit("user", { id: "u_2" }); await waitFor(() => seen.length === 2, 2000); expect(seen).toStrictEqual([{ id: "u_1" }, { id: "u_2" }]); }); it("does not require a paired-bus dep — bus stays independent", { timeout: 5000, }, () => { const { bus } = fresh(); expect(bus).toBeInstanceOf(EventBus); }); it("main-side emit of non-cloneable payload ends the subscription with UNSERIALISABLE_PAYLOAD", { timeout: 5000, }, async () => { // Cast through `any` so we can bypass the worker-side encode // probe and exercise the main-side encode-failure branch // inside `subscribeStream`. type LooseMap = { weird: unknown }; const pair = createProxyPair(); teardowns.push(pair.tearDown); const { bus, client } = pair; const seen: unknown[] = []; client.on("weird", (v) => { seen.push(v); }); await new Promise((r) => setTimeout(r, 100)); bus.emit("weird", { fn: () => undefined }); await waitFor(() => seen.length > 0, 2000); // The first observed value should be the reconstructed // EVENTS_PROXY.UNSERIALISABLE_PAYLOAD error. const dropped = seen.find( (s) => s instanceof Error && ((s as Error & { code?: string }).code === "EVENTS_PROXY.UNSERIALISABLE_PAYLOAD" || (s as Error & { code?: string }).code === "EVENTS_PROXY.SUBSCRIPTION_DROPPED") ); expect(dropped).toBeDefined(); }); });