Appearance
Layered Dispatch
When to use
Use this pattern when you need to route items to different handlers based on their type or characteristics — first matching handler wins, like Express.js route matching. Good for processing heterogeneous collections (DOM nodes, AST nodes, log entries, events) where each item needs different treatment depending on what it is.
The pattern
A Router() factory returns an object with two methods: .use() to register a predicate–handler pair, and .run() to dispatch an input to the first matching handler. The API mirrors Express's app.use() — register layers in order, first match wins.
Basic usage
Register handlers with .use(test, handler). Call .run(input) to dispatch — the first layer whose test matches receives the input:
javascript
const router = Router();
router.use(
(file) => file.endsWith(".md"),
(file) => renderMarkdown(file),
);
router.use(
(file) => file.endsWith(".csv"),
(file) => parseCSV(file),
);
router.use(
(file) => file.endsWith(".json"),
(file) => JSON.parse(readFile(file)),
);
router.run("data.csv"); // parses as CSV
router.run("README.md"); // renders as markdownCatch-all handler
Omit the test to register a catch-all that handles anything not matched by earlier layers:
javascript
const router = Router();
router.use(
(x) => x > 100,
(x) => `big: ${x}`,
);
router.use((x) => `other: ${x}`); // catch-all
router.run(200); // "big: 200"
router.run(3); // "other: 3"DOM node processing
The original use case — route DOM nodes to specialized clone/transform handlers during a tree walk. Predicates can be CSS selectors, nodeType numbers, or functions:
javascript
const router = Router();
router.use(Node.TEXT_NODE, copyTextNode);
router.use("link[rel=stylesheet]", inlineStylesheet);
router.use("script, iframe, link", stripNonVisual);
router.use("canvas", copyCanvasToImage);
router.use("img", cloneImage);
router.use(cloneAndClean); // catch-all
let node;
while ((node = walker.nextNode())) {
const out = router.run(node);
if (out) parent.appendChild(out);
}Custom predicate types
Extend Router with domain-specific predicate shorthand. The implementation below supports four forms:
| Argument type | Interpretation |
|---|---|
number | Match node.nodeType === n |
string | Match node.matches(selector) |
function | Arbitrary predicate |
| (omitted) | Catch-all (() => true) |
javascript
router.use(Node.TEXT_NODE, handler); // nodeType match
router.use("img.hero", handler); // CSS selector
router.use((n) => n.dataset.x, handler); // predicate function
router.use(handler); // catch-allImplementing Router()
The factory maintains an ordered array of layers. use() normalizes the predicate into a function and pushes a { test, handler } pair. run() iterates layers and returns the result of the first matching handler:
javascript
function Router() {
const layers = [];
function use(test, handler) {
if (!handler) {
// catch-all: test is actually the handler
layers.push({ test: () => true, handler: test });
} else if (typeof test === "number") {
layers.push({
test: (node) => node.nodeType === test,
handler,
});
} else if (typeof test === "string") {
layers.push({
test: (node) => !!node.matches?.(test),
handler,
});
} else {
layers.push({ test, handler });
}
}
function run(input) {
for (const layer of layers) {
if (layer.test(input)) {
return layer.handler(input);
}
}
}
return { use, run };
}To adapt predicate shorthand for a different domain (e.g., matching event types as strings, HTTP methods, status codes), modify the if branches in use(). The run() loop stays the same.
Trade-offs
- Pros: Declarative handler registration, easy to reorder or insert new layers, first-match semantics prevent double-handling, familiar API for anyone who's used Express.
- Cons: Linear scan — O(n) per dispatch, which is fine for dozens of layers but unsuitable for thousands. No "next" / middleware chaining — only the first match runs. If you need multiple handlers to compose on the same input, use middleware instead.
- Versus a switch statement: A switch is simpler for 2–3 branches. Once you're past 5 or layers grow complex predicates, this pattern separates registration from dispatch and makes the handler list data-driven.
- Versus a Map: A Map gives O(1) lookup but only supports exact key matching. This pattern supports predicates, selectors, and catch-alls.