Initial Commit
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
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<void>> = [];
|
||||
afterEach(async () => {
|
||||
while (teardowns.length > 0) {
|
||||
const t = teardowns.pop();
|
||||
if (t !== undefined) await t();
|
||||
}
|
||||
});
|
||||
|
||||
function fresh(highWaterMark?: number) {
|
||||
const pair = createProxyPair<Map>({
|
||||
...(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<LooseMap>();
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user