Skip to content

findAllRoutes: interpreter and compiled matchAll disagree on same-node sibling ordering #187

Closed

Description

@pi0x

Summary

findAllRoutes (interpreter) and compileRouter(router, { matchAll: true }) (compiled) return the same set of matches but in a different order when two dynamic routes share the same tree node (same param node, or same wildcard node). This is a pre-existing divergence (reproduces on main, independent of #186) surfaced while reviewing #186.

  • Interpreter emits same-node siblings in insertion order (matches.push(...match) / per-entry push in src/operations/find-all.ts).
  • Compiler emits them sorted by match weight (.sort((a,b) => b.weight - a.weight) + r.unshift(...) in src/compiler.ts compileMethodMatch / compileFinalMatch).

These two orderings only coincide when insertion order happens to line up with weight order, so the divergence is masked in most tests.

Reproduction

import { createRouter, addRoute, findAllRoutes } from "rou3";
import { compileRouter } from "rou3/compiler";

function run(routes, path) {
  const r = createRouter();
  for (const p of routes) addRoute(r, "GET", p, { path: p });
  const interp = findAllRoutes(r, "GET", path).map((m) => m.data.path);
  const compiled = compileRouter(r, { matchAll: true })("GET", path).map((m) => m.data.path);
  console.log(routes, "@", path, { interp, compiled });
}

run(["/:id/**:rest", "/:id/**"], "/a/b");
// interp:   ["/:id/**:rest", "/:id/**"]
// compiled: ["/:id/**", "/:id/**:rest"]   <- reversed

run(["/foo/:id", "/foo/*"], "/foo/x");
// interp:   ["/foo/:id", "/foo/*"]
// compiled: ["/foo/*", "/foo/:id"]        <- reversed

run(["/:id(\\d+)", "/*"], "/42");
// interp:   ["/:id(\\d+)", "/*"]
// compiled: ["/*", "/:id(\\d+)"]           <- reversed

The _findAllRoutes test helper in test/find-all.test.ts asserts expect(compiledRes).toEqual(res), so any test using these route sets/insertion orders would fail — the current suite just doesn't cover them.

Why this matters

findAllRoutes documents its result order as least→most specific (see AGENTS.md). Neither engine fully honors that for same-node siblings:

  • The interpreter uses raw insertion order, so it does not actually sort by specificity.
  • The compiler sorts by weight (≈ specificity), so it can contradict both the interpreter and insertion order.

Suggested direction

Decide the intended contract and make both engines agree:

  1. If least→most specificity is the contract, the interpreter should order same-node param/wildcard siblings by specificity rather than insertion order, and the compiler's weight model should match it.
  2. If insertion order is acceptable, document that and align the compiler to emit in insertion order for equal-specificity siblings (so matchAll output matches the interpreter).

Add regression tests using reversed-insertion route sets (the three above) once the contract is fixed — they currently expose the divergence.

Context

Found during review of #186 (which fixed a related but distinct compiler weight-inflation regression for optional ** vs required **:name). That fix restored the pre-#186 behavior for natural insertion order but does not address this broader insertion-order-vs-weight-order mismatch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions