From 077160157123455375ee386847d1637af036fdb9 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Fri, 8 May 2026 21:13:18 +0200 Subject: [PATCH] feat(core-realtime): channel-template matcher (placeholder extraction) --- .../src/channel-template.test.ts | 33 +++++++++++++++++++ .../core-realtime/src/channel-template.ts | 28 ++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 packages/core-realtime/src/channel-template.test.ts create mode 100644 packages/core-realtime/src/channel-template.ts diff --git a/packages/core-realtime/src/channel-template.test.ts b/packages/core-realtime/src/channel-template.test.ts new file mode 100644 index 0000000..39ff016 --- /dev/null +++ b/packages/core-realtime/src/channel-template.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from "vitest"; +import { matchChannelTemplate } from "@/channel-template"; + +describe("matchChannelTemplate", () => { + it("matches a plain channel name exactly", () => { + expect(matchChannelTemplate("blog.feed", "blog.feed")).toEqual({ params: {} }); + expect(matchChannelTemplate("blog.feed", "blog.other")).toBeNull(); + }); + + it("matches a templated channel and extracts params", () => { + expect( + matchChannelTemplate("notifications.user.{userId}", "notifications.user.user_42"), + ).toEqual({ params: { userId: "user_42" } }); + }); + + it("returns null when a templated channel doesn't match the shape", () => { + expect( + matchChannelTemplate("notifications.user.{userId}", "notifications.user"), + ).toBeNull(); + expect( + matchChannelTemplate("notifications.user.{userId}", "blog.feed"), + ).toBeNull(); + }); + + it("supports multiple placeholders", () => { + expect( + matchChannelTemplate( + "rooms.{roomId}.user.{userId}", + "rooms.r1.user.u1", + ), + ).toEqual({ params: { roomId: "r1", userId: "u1" } }); + }); +}); diff --git a/packages/core-realtime/src/channel-template.ts b/packages/core-realtime/src/channel-template.ts new file mode 100644 index 0000000..7247a3d --- /dev/null +++ b/packages/core-realtime/src/channel-template.ts @@ -0,0 +1,28 @@ +const PLACEHOLDER = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g; + +export function matchChannelTemplate( + template: string, + candidate: string, +): { params: Record } | null { + // No placeholders: exact match. + if (!template.includes("{")) { + return template === candidate ? { params: {} } : null; + } + + // Build a regex from the template, replacing {name} with named groups. + const names: string[] = []; + const escaped = template.replace(/[.+?^${}()|[\]\\]/g, "\\$&"); // escape regex specials + // The above escapes `{` and `}` too — restore them around placeholders. + const pattern = escaped.replace(/\\\{([a-zA-Z_][a-zA-Z0-9_]*)\\\}/g, (_m, name) => { + names.push(name); + return `([^.]+)`; + }); + const re = new RegExp(`^${pattern}$`); + const match = candidate.match(re); + if (!match) return null; + const params: Record = {}; + names.forEach((name, i) => { + params[name] = match[i + 1]!; + }); + return { params }; +}