UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

89 lines 3.88 kB
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