@beignet/core
Version:
Core framework primitives for Beignet
89 lines • 3.88 kB
JavaScript
import { eventTransportValuesEqual, toEventTransportValue, } from "../events/transport.js";
/** Protocol defaults and safety limits shared by clients, endpoints, and providers. */
export const broadcastLimits = Object.freeze({
subscriptions: 20,
requestBytes: 8_192,
payloadBytes: 65_536,
bufferedBytes: 1_048_576,
defaultLifetimeMs: 60_000,
maxLifetimeMs: 3_600_000,
lifetimeGraceMs: 5_000,
heartbeatMs: 25_000,
readyTimeoutMs: 10_000,
});
/** Invalid channel configuration or untrusted protocol input. */
export class BroadcastValidationError extends Error {
constructor(message) {
super(message);
this.name = "BroadcastValidationError";
}
}
const namePattern = /^[a-zA-Z][a-zA-Z0-9_.-]{0,127}$/;
export function defineChannel(name, options) {
if (!namePattern.test(name))
throw new BroadcastValidationError("Invalid broadcast channel name");
const events = Object.entries(options.events);
if (!events.length || events.some(([event]) => !namePattern.test(event))) {
throw new BroadcastValidationError(`Channel ${name} needs valid event names`);
}
return Object.freeze({
kind: "channel",
name,
params: options.params,
events: Object.freeze({ ...options.events }),
});
}
/** Validate both sides of a transport boundary; schema transforms must be stable. */
async function parseStable(schema, input, label) {
try {
const parsed = await schema["~standard"].validate(input);
if (parsed.issues)
throw new BroadcastValidationError(`Invalid ${label}`);
const canonical = toEventTransportValue(label, parsed.value);
const second = await schema["~standard"].validate(canonical);
if (second.issues ||
!eventTransportValuesEqual(canonical, toEventTransportValue(label, second.value))) {
throw new BroadcastValidationError(`Unstable transport schema for ${label}`);
}
return canonical;
}
catch (error) {
if (error instanceof BroadcastValidationError)
throw error;
throw new BroadcastValidationError(`Invalid ${label}`);
}
}
/** Provider and transport boundary validation. Uses parsed, canonical parameters. */
export async function parseChannelParams(channel, input) {
const parsed = await parseStable(channel.params, input, `${channel.name} parameters`);
if (!parsed ||
typeof parsed !== "object" ||
Array.isArray(parsed) ||
Object.values(parsed).some((value) => typeof value !== "string")) {
throw new BroadcastValidationError(`Channel ${channel.name} parameters must be a flat string record`);
}
if (new TextEncoder().encode(JSON.stringify(parsed)).byteLength >
broadcastLimits.requestBytes) {
throw new BroadcastValidationError(`Channel ${channel.name} parameters are too large`);
}
return parsed;
}
/** Provider and browser boundary validation. Event names select their schema. */
export async function parseChannelEvent(channel, input) {
if (!Object.hasOwn(channel.events, input.event))
throw new BroadcastValidationError(`Unknown event on channel ${channel.name}`);
const data = await parseStable(channel.events[input.event], input.data, `${channel.name}.${input.event}`);
if (new TextEncoder().encode(JSON.stringify(data)).byteLength >
broadcastLimits.payloadBytes) {
throw new BroadcastValidationError(`Channel ${channel.name} payload is too large`);
}
return { event: input.event, data };
}
/** Collision-free identity independent of parameter property order. Parse parameters first. */
export function channelKey(channel, params) {
return JSON.stringify([
channel,
Object.entries(params).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
]);
}
//# sourceMappingURL=index.js.map