feat: add native module realtime transport
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
MODULE_REALTIME_CHANNEL_MAX_LENGTH,
|
||||
MODULE_REALTIME_ERROR_CODES,
|
||||
MODULE_REALTIME_MAX_CHANNELS_PER_MODULE,
|
||||
MODULE_REALTIME_MAX_PAYLOAD_BYTES,
|
||||
MODULE_REALTIME_MAX_SUBSCRIBERS_PER_CHANNEL,
|
||||
ModuleRealtimeError,
|
||||
type ModuleRealtimePublisher,
|
||||
parseModuleRealtimeChannel,
|
||||
parseModuleRealtimeChannelDefinition,
|
||||
safeParseModuleRealtimeChannel,
|
||||
safeParseModuleRealtimeChannelDefinition,
|
||||
} from "../src/index.js";
|
||||
|
||||
describe("module realtime contracts", () => {
|
||||
it("accepts an ordinary named state interface without an index signature or cast", () => {
|
||||
interface SlideshowState {
|
||||
readonly currentSlide: number;
|
||||
readonly mode: "on" | "off";
|
||||
readonly slides: readonly string[];
|
||||
}
|
||||
const state: SlideshowState = { currentSlide: 1, mode: "on", slides: ["/welcome.png"] };
|
||||
const received: unknown[] = [];
|
||||
const publisher: ModuleRealtimePublisher = {
|
||||
publish: (_channel, payload) => received.push(payload),
|
||||
};
|
||||
|
||||
publisher.publish("state", state);
|
||||
expect(received).toEqual([state]);
|
||||
});
|
||||
|
||||
it("accepts flat local kebab-case channels and rejects hierarchy and wildcards", () => {
|
||||
expect(parseModuleRealtimeChannel("state")).toBe("state");
|
||||
expect(parseModuleRealtimeChannel("slide-state")).toBe("slide-state");
|
||||
for (const invalid of ["", "State", "slide_state", "slide.state", "slide/state", "*", "a.*"]) {
|
||||
expect(safeParseModuleRealtimeChannel(invalid).success).toBe(false);
|
||||
}
|
||||
expect(
|
||||
safeParseModuleRealtimeChannel("a".repeat(MODULE_REALTIME_CHANNEL_MAX_LENGTH)).success,
|
||||
).toBe(true);
|
||||
expect(
|
||||
safeParseModuleRealtimeChannel("a".repeat(MODULE_REALTIME_CHANNEL_MAX_LENGTH + 1)).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("parses only the exact channel and permission definition shape", () => {
|
||||
expect(
|
||||
parseModuleRealtimeChannelDefinition({
|
||||
channel: "state",
|
||||
permission: "announcements.slideshow.view",
|
||||
}),
|
||||
).toEqual({ channel: "state", permission: "announcements.slideshow.view" });
|
||||
expect(
|
||||
safeParseModuleRealtimeChannelDefinition({
|
||||
channel: "state",
|
||||
permission: "announcements.slideshow.view",
|
||||
topic: "legacy",
|
||||
}),
|
||||
).toEqual({ success: false, code: "MODULE_REALTIME_CHANNEL_DEFINITION_INVALID" });
|
||||
expect(safeParseModuleRealtimeChannelDefinition({ channel: "state", permission: "*" })).toEqual(
|
||||
{ success: false, code: "MODULE_REALTIME_PERMISSION_INVALID" },
|
||||
);
|
||||
});
|
||||
|
||||
it("publishes finite platform limits as explicit contract constants", () => {
|
||||
expect(MODULE_REALTIME_MAX_CHANNELS_PER_MODULE).toBe(16);
|
||||
expect(MODULE_REALTIME_MAX_SUBSCRIBERS_PER_CHANNEL).toBe(100);
|
||||
expect(MODULE_REALTIME_MAX_PAYLOAD_BYTES).toBe(65_536);
|
||||
});
|
||||
|
||||
it("uses a closed error-code set with fixed sanitized messages and stacks", () => {
|
||||
expect(MODULE_REALTIME_ERROR_CODES).toContain("REALTIME_PAYLOAD_INVALID");
|
||||
const error = new ModuleRealtimeError("REALTIME_PAYLOAD_INVALID");
|
||||
expect(error).toMatchObject({
|
||||
code: "REALTIME_PAYLOAD_INVALID",
|
||||
message: "The realtime payload is not JSON-compatible.",
|
||||
name: "ModuleRealtimeError",
|
||||
});
|
||||
expect(error.stack).toBe("ModuleRealtimeError: The realtime payload is not JSON-compatible.");
|
||||
expect(Object.isFrozen(error)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user