/** * Patterns are dot-segmented globs. `*` matches exactly one segment; * `**` matches one or more segments. Segments are anything that's not * a dot. * * deploy.completed.* matches deploy.completed.lunacycle * no match deploy.completed.lunacycle.beta (extra segment) * *.failed matches deploy.failed * deploy.** matches deploy.completed.lunacycle.beta * * For SQL pre-filtering we compile a `LIKE` pattern that's a superset * of the real match set. JS-side `matchPattern` does the exact check. * Storing both keeps emit() fast (LIKE is indexable) without giving up * single-segment semantics. * * `_` and `%` in literal segments are escaped with `\` so they're * treated as literal characters rather than SQL LIKE wildcards. The * companion query in bus.ts must include `ESCAPE '\'` for this to * take effect — see {@link LIKE_ESCAPE}. */ /** * SQL ESCAPE-clause character. Both this module and the LIKE query * in bus.ts must agree on it; export it so callers can construct * compatible queries without re-deriving the choice. */ export const LIKE_ESCAPE = '\\'; export interface CompiledPattern { pattern: string; likePattern: string; segmentCount: number; // -1 if pattern contains `**` (variable) regex: RegExp; } const SEGMENT_RE = /[^.]+/; export function compilePattern(pattern: string): CompiledPattern { if (pattern.length === 0) { throw new Error('event-bus: pattern cannot be empty'); } const segments = pattern.split('.'); let likePattern = ''; let regexBody = ''; let hasDoubleStar = false; for (let i = 0; i < segments.length; i++) { const seg = segments[i]; if (i > 0) { likePattern += '.'; regexBody += '\\.'; } if (seg === '**') { hasDoubleStar = true; likePattern += '%'; // `**` matches one-or-more segments regexBody += '[^.]+(?:\\.[^.]+)*'; } else if (seg === '*') { likePattern += '%'; regexBody += '[^.]+'; } else { if (!SEGMENT_RE.test(seg)) { throw new Error(`event-bus: invalid pattern segment "${seg}" in "${pattern}"`); } likePattern += escapeForLike(seg); regexBody += escapeForRegex(seg); } } return { pattern, likePattern, segmentCount: hasDoubleStar ? -1 : segments.length, regex: new RegExp(`^${regexBody}$`), }; } export function matchPattern(compiled: CompiledPattern, type: string): boolean { return compiled.regex.test(type); } function escapeForLike(literal: string): string { // Bun's sqlite uses default LIKE semantics: `%` matches zero+ chars, // `_` matches exactly one. We use `\` as the ESCAPE character, so // also escape `\` itself. The companion query in bus.ts must use // `ESCAPE '\'` for SQLite to honor these escapes. return literal.replace(/[\\_%]/g, '\\$&'); } function escapeForRegex(literal: string): string { return literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }