From 07c2438fb87e9fe8dbb41e1401e0053f20911c4a Mon Sep 17 00:00:00 2001 From: nav Date: Wed, 8 Apr 2026 22:39:49 +0000 Subject: [PATCH] feat: add client rule registry --- plugin/core/rules.ts | 85 ++++++++++++++++++++++++++++++++++++++++++++ plugin/index.ts | 7 ++++ 2 files changed, 92 insertions(+) create mode 100644 plugin/core/rules.ts diff --git a/plugin/core/rules.ts b/plugin/core/rules.ts new file mode 100644 index 0000000..3d8c2ea --- /dev/null +++ b/plugin/core/rules.ts @@ -0,0 +1,85 @@ +import { BUILTIN_RULE, CodecError, parseRuleMessage } from "../../../Yonexus.Protocol/src/index.js"; + +export type ClientRuleProcessor = (message: string) => unknown; + +export class ClientRuleRegistryError extends Error { + constructor(message: string) { + super(message); + this.name = "ClientRuleRegistryError"; + } +} + +export interface ClientRuleRegistry { + readonly size: number; + registerRule(rule: string, processor: ClientRuleProcessor): void; + hasRule(rule: string): boolean; + dispatch(raw: string): boolean; + getRules(): readonly string[]; +} + +export class YonexusClientRuleRegistry implements ClientRuleRegistry { + private readonly rules = new Map(); + + get size(): number { + return this.rules.size; + } + + registerRule(rule: string, processor: ClientRuleProcessor): void { + const normalizedRule = this.normalizeRule(rule); + if (this.rules.has(normalizedRule)) { + throw new ClientRuleRegistryError( + `Rule '${normalizedRule}' is already registered` + ); + } + + this.rules.set(normalizedRule, processor); + } + + hasRule(rule: string): boolean { + return this.rules.has(rule.trim()); + } + + dispatch(raw: string): boolean { + const parsed = parseRuleMessage(raw); + const processor = this.rules.get(parsed.ruleIdentifier); + if (!processor) { + return false; + } + + processor(raw); + return true; + } + + getRules(): readonly string[] { + return [...this.rules.keys()]; + } + + private normalizeRule(rule: string): string { + const normalizedRule = rule.trim(); + if (!normalizedRule) { + throw new ClientRuleRegistryError("Rule identifier must be a non-empty string"); + } + + if (normalizedRule === BUILTIN_RULE) { + throw new ClientRuleRegistryError( + `Rule identifier '${BUILTIN_RULE}' is reserved` + ); + } + + try { + parseRuleMessage(`${normalizedRule}::probe`); + } catch (error) { + if (error instanceof CodecError) { + throw new ClientRuleRegistryError(error.message); + } + + throw error; + } + + return normalizedRule; + } +} + +export function createClientRuleRegistry(): ClientRuleRegistry { + return new YonexusClientRuleRegistry(); +} diff --git a/plugin/index.ts b/plugin/index.ts index 9b7857f..e17e8fc 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -31,6 +31,13 @@ export { type YonexusClientRuntimeState, type YonexusClientPhase } from "./core/runtime.js"; +export { + createClientRuleRegistry, + YonexusClientRuleRegistry, + ClientRuleRegistryError, + type ClientRuleRegistry, + type ClientRuleProcessor +} from "./core/rules.js"; export interface YonexusClientPluginManifest { readonly name: "Yonexus.Client";