awscdk-construct-scte-scheduler
Version:
AWS CDK Construct for scheduling SCTE-35 events using the MediaLive schedule API
210 lines (189 loc) • 6.58 kB
JavaScript
/**
* ExpressionSet - An indexed collection of Expressions for efficient bulk matching
*
* Instead of iterating all expressions on every tag, ExpressionSet pre-indexes
* them at insertion time by depth and terminal tag name. At match time, only
* the relevant bucket is evaluated — typically reducing checks from O(E) to O(1)
* lookup plus O(small bucket) matches.
*
* Three buckets are maintained:
* - `_byDepthAndTag` — exact depth + exact tag name (tightest, used first)
* - `_wildcardByDepth` — exact depth + wildcard tag `*` (depth-matched only)
* - `_deepWildcards` — expressions containing `..` (cannot be depth-indexed)
*
* @example
* import { Expression, ExpressionSet } from 'fast-xml-tagger';
*
* // Build once at config time
* const stopNodes = new ExpressionSet();
* stopNodes.add(new Expression('root.users.user'));
* stopNodes.add(new Expression('root.config.setting'));
* stopNodes.add(new Expression('..script'));
*
* // Query on every tag — hot path
* if (stopNodes.matchesAny(matcher)) { ... }
*/
export default class ExpressionSet {
constructor() {
/** @type {Map<string, import('./Expression.js').default[]>} depth:tag → expressions */
this._byDepthAndTag = new Map();
/** @type {Map<number, import('./Expression.js').default[]>} depth → wildcard-tag expressions */
this._wildcardByDepth = new Map();
/** @type {import('./Expression.js').default[]} expressions containing deep wildcard (..) */
this._deepWildcards = [];
/** @type {Set<string>} pattern strings already added — used for deduplication */
this._patterns = new Set();
/** @type {boolean} whether the set is sealed against further additions */
this._sealed = false;
}
/**
* Add an Expression to the set.
* Duplicate patterns (same pattern string) are silently ignored.
*
* @param {import('./Expression.js').default} expression - A pre-constructed Expression instance
* @returns {this} for chaining
* @throws {TypeError} if called after seal()
*
* @example
* set.add(new Expression('root.users.user'));
* set.add(new Expression('..script'));
*/
add(expression) {
if (this._sealed) {
throw new TypeError(
'ExpressionSet is sealed. Create a new ExpressionSet to add more expressions.'
);
}
// Deduplicate by pattern string
if (this._patterns.has(expression.pattern)) return this;
this._patterns.add(expression.pattern);
if (expression.hasDeepWildcard()) {
this._deepWildcards.push(expression);
return this;
}
const depth = expression.length;
const lastSeg = expression.segments[expression.segments.length - 1];
const tag = lastSeg?.tag;
if (!tag || tag === '*') {
// Can index by depth but not by tag
if (!this._wildcardByDepth.has(depth)) this._wildcardByDepth.set(depth, []);
this._wildcardByDepth.get(depth).push(expression);
} else {
// Tightest bucket: depth + tag
const key = `${depth}:${tag}`;
if (!this._byDepthAndTag.has(key)) this._byDepthAndTag.set(key, []);
this._byDepthAndTag.get(key).push(expression);
}
return this;
}
/**
* Add multiple expressions at once.
*
* @param {import('./Expression.js').default[]} expressions - Array of Expression instances
* @returns {this} for chaining
*
* @example
* set.addAll([
* new Expression('root.users.user'),
* new Expression('root.config.setting'),
* ]);
*/
addAll(expressions) {
for (const expr of expressions) this.add(expr);
return this;
}
/**
* Check whether a pattern string is already present in the set.
*
* @param {import('./Expression.js').default} expression
* @returns {boolean}
*/
has(expression) {
return this._patterns.has(expression.pattern);
}
/**
* Number of expressions in the set.
* @type {number}
*/
get size() {
return this._patterns.size;
}
/**
* Seal the set against further modifications.
* Useful to prevent accidental mutations after config is built.
* Calling add() or addAll() on a sealed set throws a TypeError.
*
* @returns {this}
*/
seal() {
this._sealed = true;
return this;
}
/**
* Whether the set has been sealed.
* @type {boolean}
*/
get isSealed() {
return this._sealed;
}
/**
* Test whether the matcher's current path matches any expression in the set.
*
* Evaluation order (cheapest → most expensive):
* 1. Exact depth + tag bucket — O(1) lookup, typically 0–2 expressions
* 2. Depth-only wildcard bucket — O(1) lookup, rare
* 3. Deep-wildcard list — always checked, but usually small
*
* @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view)
* @returns {boolean} true if any expression matches the current path
*
* @example
* if (stopNodes.matchesAny(matcher)) {
* // handle stop node
* }
*/
matchesAny(matcher) {
return this.findMatch(matcher) !== null;
}
/**
* Find and return the first Expression that matches the matcher's current path.
*
* Uses the same evaluation order as matchesAny (cheapest → most expensive):
* 1. Exact depth + tag bucket
* 2. Depth-only wildcard bucket
* 3. Deep-wildcard list
*
* @param {import('./Matcher.js').default} matcher - Matcher instance (or readOnly view)
* @returns {import('./Expression.js').default | null} the first matching Expression, or null
*
* @example
* const expr = stopNodes.findMatch(matcher);
* if (expr) {
* // access expr.config, expr.pattern, etc.
* }
*/
findMatch(matcher) {
const depth = matcher.getDepth();
const tag = matcher.getCurrentTag();
// 1. Tightest bucket — most expressions live here
const exactKey = `${depth}:${tag}`;
const exactBucket = this._byDepthAndTag.get(exactKey);
if (exactBucket) {
for (let i = 0; i < exactBucket.length; i++) {
if (matcher.matches(exactBucket[i])) return exactBucket[i];
}
}
// 2. Depth-matched wildcard-tag expressions
const wildcardBucket = this._wildcardByDepth.get(depth);
if (wildcardBucket) {
for (let i = 0; i < wildcardBucket.length; i++) {
if (matcher.matches(wildcardBucket[i])) return wildcardBucket[i];
}
}
// 3. Deep wildcards — cannot be pre-filtered by depth or tag
for (let i = 0; i < this._deepWildcards.length; i++) {
if (matcher.matches(this._deepWildcards[i])) return this._deepWildcards[i];
}
return null;
}
}