Skip to content

typescript, javascript: Hono route table, middleware chains, context keys, request links and call chains (#2003) - #2004

Open
swapnilpaliwal-sd wants to merge 9 commits into
0.2.3from
release/hono-0.2.3
Open

swapnilpaliwal-sd wants to merge 9 commits into
0.2.3from
release/hono-0.2.3

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2003

Merges into the 0.2.3 release branch, which then goes to dev; not proposed for merge before the 0.2.3 release.

Before merge (open for review):

  • How "no new false chains" is counted. No edge the compiler refutes is added on any measured
    project (one project loses 6). On one held-out project the number of graph paths that pass
    through an already refuted edge rises, from 784 of 55,081 scored paths (1.42%) to 1,015 of
    58,742 (1.73%): the new correct edges let more paths reach the same 876 refuted edges, which
    are identical before and after. Per edge the condition holds; per path it does not on that
    project. Without it, held-out false paths are 42 before and 42 after.

What this adds

The TypeScript and JavaScript engines now read what a Hono app declares and what it does with a
request, and follow a request from its route into the code the handler calls. Each graph stays
on its own; nothing links the two languages.

An agent can now ask:

  • Routes with their full paths. Every verb, on, all and route() registration becomes a
    route with its composed path, through basePath, nested mounts, sub-apps built by factories or
    registration functions or returned from a function, declarative route lists, OpenAPI routes
    (also through a project's own wrapper around createRoute), and an app variable re-pointed at a
    basePath result. A path piece that is not a literal or a const is reported as undetermined
    with a reason, never guessed.
  • Middleware chains in order. Each route lists the middleware that runs before its handler
    (app-wide and path-scoped use), in registration order, tagged when a registration sits inside
    a branch or a loop.
  • Context keys. c.set(key) in a middleware links to the c.get(key) / c.var.key /
    const { key } = c.var reads in the handlers it runs before; a set made after await next()
    does not.
  • Typed-client and test-request links. An hc client call, a fetch to a path the program
    spells, and app.request(..) in a test link to the route they reach, with the method taken from
    the request. A request whose method or base cannot be decided says so instead of joining at
    random.
  • Validator schemas. A zod schema given to a validator links to the handler that reads it
    through c.req.valid(..).
  • Call chains below the handler. A service read out of the framework context
    (const { orders } = c.get(key), then orders.create(..)), element types of arrays and tables,
    this inside arrow functions, function values handed to a callee that runs them (a new
    callback_handed edge tier, ranked as a hand-off), and decided table keys. A function handed
    to code that never runs it (a getter read, an unread or only-compared parameter, a keyed
    registry slot) gets no edge.
  • Declared unknowns. What the rules cannot decide is a row naming the reason.

impact, context and path answer on all of these with existing options only.

Placement: the framework rules live in each engine's config-resolution/ and
framework-behavior/ phases; framework names appear only in the knob files. Generic,
framework-free changes outside them are listed in the commit messages.

Measured on this branch merged with the current 0.2.3

Against a clean checkout of 0.2.3 (which includes #2002) run the same way.

suite 0.2.3 this PR merged
TypeScript regression (graph/test/typescript/run-tests.sh, preflight incl. compiler-load lint) 106/106 107/107 (one new case; 14 goldens updated, each row checked against the fixture; see the commit message)
JavaScript regression 88/88 88/88
tests/run.py --lang typescript 356/356 checks 361/361 checks
tests/run.py --lang javascript 354/354 checks 354/354 checks
parser suites (TypeScript, JavaScript, Deno) pass pass
Hono suite, TypeScript (--oracle, bare and library variants) n/a 170/170 (incl. 9 call-chain cases)
Hono suite, JavaScript (--oracle) n/a 25/25 (incl. 1 call-chain case)

No suite fails on either side.

Call-chain coverage

Twenty-two projects (12 dev, 10 held out; TypeScript and JavaScript, Hono and not), each indexed
by both engines from the same copy. Static truth is a TypeScript-compiler call oracle plus an
independent route oracle. "Before" is the route model alone; it leaves the compiler call layer
exactly as on dev.

measure before this PR
compiler call chains complete end to end, dev 0.777 0.942
same, held out 0.609 0.665
same, held out without its largest project 0.853 0.929
chain-edge precision (dev / held out) 0.992 / 0.964 0.992 / 0.964
request chains complete end to end (route, middleware, handler, calls), dev / held out 0.837 / 0.548 0.946 / 0.613
refuted edges added 0 (one project loses 6)

Runtime: chains recorded from projects' own test suites (no network services), each hop matched
to the graph. Held-out projects, chains observed end to end: 0.364 -> 0.510; one project's
traced request chains 4,848 -> 6,722 of 14,022.

Against dev, the route model alone already takes routes whose handler the graph reaches from
0/413 to 413/413, middleware -> handler hops from 0/2488 to 2488/2488, and context answers on a
request from 0/24 to 24/24.

Index time

CPU time (user + sys) and peak RSS in ABBA rounds, both sides on the same local build mode,
against a noise band from repeated runs of one engine. The rule is that the same work must not
get slower; extra time that produces newly resolved output is expected.

  • Whole index, call chains added vs route model alone, two rounds per project: -1.3% to +5.2%,
    every project inside its noise band; peak RSS within +28 MB; every graph grew.
  • Solve only, on saved facts (more precise): the call chains add 0.5 to 1.8 s per project. On the
    two projects examined per relation, 80% and 56% of the added time is in relations that gain
    rows; the rest is spread thinly (no relation above 0.09 s).
  • The route model's own cost was profiled per rule on the largest project: rules that added time
    without adding output were rewritten with identical output (solve 28.2 -> 23.8 s); what remains
    over the base is the per-instance route and middleware expansion that produces that project's
    new routes (208 -> 832) and middleware applications (1,750 -> 8,147). On a program that does not
    import the framework the router rules do no work.

What is not in this PR

  • One workspace-subpath test case that needs a parser change outside this release.
  • No version bump of its own: the 0.2.3 branch carries the 0.2.3 version change.

Merge conditions

  • Call-chain coverage rises against dev (table above), with chain-edge precision not falling
    and no new false chains (see "Before merge" on how that is counted).
  • No slowdown on the same work, and no peak RSS growth beyond the noise band (Index time, above).

@swapnilpaliwal-sd swapnilpaliwal-sd added typescript TypeScript javascript JavaScript engine Resolution / call-graph engine rules enhancement New feature or request labels Oct 29, 2026
@swapnilpaliwal-sd
swapnilpaliwal-sd changed the base branch from dev to 0.2.3 October 29, 2026 16:41
…s, context keys and request links

The engine now reads what a Hono app declares and what it does with a request, in both the
TypeScript and the JavaScript graphs (each graph on its own, nothing links the two).

What an agent can now ask:

- Routes with their full paths. Every get/post/on/all/route registration becomes a route with
  its verb and its composed path, through basePath, route() mounts several levels deep,
  sub-apps built by factories or registration functions, declarative route lists, and an app
  variable re-pointed at a basePath result (exact when unconditional, partial when it is not). A path
  that is not a literal or a const is reported as undetermined with a reason, never guessed.
- Middleware chains in order. Each route lists the middleware that runs before its handler,
  app-wide use("*") and path-scoped use(..) included, in registration order, with a tag when a
  registration sits inside a branch or a loop.
- Context keys. A c.set(key) in a middleware is linked to the c.get(key) and c.var.key reads in
  the handlers it runs before.
- Typed-client and test-request links. An hc client call, a fetch to a path the program spells,
  and app.request(..) in a test are linked to the route they reach. A request whose method or
  base cannot be decided says so instead of joining at random.
- Validator schemas. A zod schema handed to a validator is linked to the handler that reads it
  through c.req.valid(..), including schemas built by project helpers.
- Declared unknowns. Anything the rules cannot decide (a dynamic path piece, an unresolvable
  middleware list, a run-time base path) is a row that names the reason.

impact, context and path answer on all of these: "how does a request to POST /x run" names the
route, its handler and the middleware in front of it in order; path from a middleware to a
handler goes through the route; impact on a middleware lists every route it runs before.

The rules live in the config-resolution and framework-behavior phases of each engine, and the
framework names only in the knob files. In the TypeScript engine, every router rule that starts
from a program-wide relation runs only when the program imports a module a router knob names, so a
program without such an import pays nothing for them; the served-app fetch lookup is a keyed join. Generic changes outside them, framework-free:

- resolution: what `await import('./m')` loads, and the method a loop over a const array of
  typed elements calls (read by the path evaluation only, it draws no call edge);
- the string evaluation that names a request's path, shared by every HTTP framework, now
  follows class fields, a parameter of a function called once and a zero-argument call, stops
  trusting a variable that is reassigned, and no longer reads a fetch of a module-relative
  file URL as a request;
- a backtick string with no substitution counts as a literal path for entry points.
- the bundle indexes the new router relations the queries read (EXT_QUERIED), as it does the
  other queried ext tables.

The existing Express and Nest route suites give the same results as before.

Tests: graph/test/typescript/hono and graph/test/javascript/hono, every case checked against an
independent oracle built on the TypeScript compiler. Whole request call chains (handler into
service into repository) are not in this change.
…e after next()

A handler that reads a context key with `const { key } = c.var` is now a
reader of the middleware that sets it. A middleware's `c.set` made after
the `next()` that runs on every path is no longer linked to later
functions on the chain: it happens once they have returned. A `next()`
in a branch, loop or try (`if (skip) return next()`) does not count.

The Hono oracle had the same gaps, and also credited a context access in
a callback nested in a handler to the callback; it now credits the
function that owns `c`. New cases cover each shape in TypeScript and
JavaScript.

Fixes #2003
…napi routes built through a project wrapper, one route row per route, and partial routes with a hole in the middle of their path

Route model fixes:

- A function that returns an object holding several sub-apps: each property read of that
  object (rt.stock, or a destructured name) mounts only its own sub-app, not every app the
  function created. This also holds when the object is passed into another function and
  destructured there. It used to put every sub-app under every mount, which made false routes.
- A test helper that writes the init of app.request(path, init) in place now joins on the
  method it writes. A method that is not a literal verb joins any route on that path.
- Middleware read off an object a factory returns (guard.middleware, or a destructured
  name) is the function the factory put there.
- openapi(def, handler) follows a project wrapper that returns createRoute({...config,
  middleware}). Method and path come from the call of the wrapper, also when the wrapper
  passes them on through its own parameters, and middleware comes from the wrapper,
  including a spread of the caller's list. A definition that cannot be read is an
  undetermined route with the reason dynamic_route_path, never dropped. A {} hole is no
  longer turned into a :param segment.
- JavaScript gets the openapi model: openapi(def, handler) registers the route its createRoute
  definition describes (verb, path with {x} read as :x, middleware), directly or through a
  project wrapper. It lives in a new config-resolution/http-router-openapi.dl; the framework
  names are knobs.
- http_route keeps one row per handler, verb, path and app, with the earliest registration
  key. An app factory called from many places used to give one row per call path, about a
  thousand rows per route on a large project.
- An app that a request handler serves only by calling its fetch (a forwarding handler) is a
  partial route with the reason unresolved_mount, not a root served at its own paths.
- context answers a full path with a partial route whose unknown piece sits in the middle of
  its path (/v1/{}/ping), with a tests/run.py case and a control that is not that route.

Hono test oracle: a brace glob alternative that holds a * is read as a glob, each row is
printed once (five goldens lose repeated rows, the row set is unchanged), a destructuring
initializer is evaluated once, and a case can opt into rows that pin the known path of a
partial route, which the runtime check also verifies.

New cases: TypeScript compose/glob-brace-alternatives, compose/partial-path-mid-hole,
compose/returned-object-destructure, compose/returned-object-member,
middleware/factory-object-member, openapi/create-route-wrapper, rpc/helper-request-method;
JavaScript js/cjs-factory-object-mw, js/cjs-returned-object-member, js/esm-create-route-wrapper,
js/esm-helper-request-method, js/esm-openapi-create-route, js/esm-returned-object-destructure.
@swapnilpaliwal-sd
swapnilpaliwal-sd marked this pull request as ready for review October 30, 2026 02:09
…t prefix scan and openapi definition lookups do the same work in a fraction of the time

- The loop-mount prefix scan read every middleware key of every app character by character; it now scans only the keys that hold a loop-mount mark.
- Scope and path pairs were built through every (middleware key, handler key) pair; they are now built from each root's distinct scopes and distinct paths. A pair is only consulted beside such a key pair and a segment's kind depends on the segment alone, so the decisions are the same.
- The param_vs_literal_scope declaration starts from the few undecidable (scope, path) pairs instead of every key pair.
- The earliest registration key of a route is taken once per (handler, verb, path, app) instead of once per row of the group.
- Two recursive rules (a route definition read through a namespace import, and a call through a destructured name) start from their recursive atom instead of rescanning every property or variable read each round.
- Object literals and parameters are read only for the expressions a route definition is taken from, never for every object literal and parameter read in the program.

Every output relation is identical, as a sorted multiset, on six projects. On the largest of them the solve CPU goes from 34.6 s to 24.8 s.
…er no longer serves one call's sub-app under another call's app

The returned-object-through-a-parameter rule took every call's object at once, so with attach(host, ordersModule()) and attach(host, invoicesModule()) each host served both modules' routes. When objects returned by two different functions reach a parameter that a mount call in its function reads its sub-app from, that parameter no longer carries them, and the sub-apps they hold are declared unresolved_mount instead. One function's results reaching it (the same factory called at several sites) keep their routes. Which functions reach a parameter is decided from calls and bindings alone, before any app is known.

A tests/run.py case pins both shapes: the two false routes the previous rules printed are gone, and the same-factory control keeps its routes.
… arrows, decided table keys, functions handed to project functions, and context values to the services they hold

Rules that close breaks in request call chains, each framework-free unless named:

- Element types: a value read as an element of an array or tuple, or of what a function returns without a written return type, has the element's type; tuple destructuring gives each name its own position's type, and a member of an object literal shape keeps the type of what was written there.
- this inside an arrow function is the this of the method or function that encloses it, through any depth of nested arrows.
- A table of functions read under a decided key (a literal, a const, or a const-object member) runs only that key's entry. A key known only at run time stays an unknown call instead of reaching every entry; a typed table's entries under such a key are value candidates of its index signature.
- A function handed to a project function (withRetry(2, () => load(id)), apply(upper, s)) is a hop from the hand-over site, under the new tier callback_handed, where the callee's body can run it. It is not emitted for a getter read, for a parameter the body never reads or only compares, or for a keyed registry's slot, which the publish on the same key runs.
- Hono: a value read from the request context (c.get, c.var, destructured c.var) has the type the app's environment declares for its key, or, when it declares none, the type of what the app's own middleware writes under that key. A context key also flows along a partial route's decided chain. JavaScript gets the same context-value link, and exports its dispatch and function-value envelope under the TypeScript file names.
- The string evaluation reads a called function's returned root instead of joining every returned sub-expression with its parents, which keeps the index cost flat.

The bundle schema documents callback_handed as a hand-over tier, and the chain reader ranks it beside callback_registered. The TypeScript edge normalizer skips it in compiler pairs, as it skips callback_registered.

Fourteen TypeScript regression goldens change, each row checked against its fixture. Each added callback_handed row is a function handed to a project function whose body calls or passes on that parameter: apply calls t (08), fromArgument calls pick (09), withSink calls run (34), $constructor calls init (59), makeThing and useMemoLike call f (75), each calls fn (78-hof-callback-at-library-boundary), Set.forEach, Promise.then, Lazy.then and Lazy.map call cb (81), and Pipeline.map hands project to Array.map (82). In 80-generic-return-receiver the elements of a typed table read under a computed key move from value_call to value candidates of the index signature. In 78-callable-collections the two value_call rows of an untyped table read under a computed key are dropped; the call stays ambiguous_unknown.
…Hono oracle and a runtime trace

A chains/ case pins, per request, the chain of functions it runs: middleware in order, the handler, and the services and helpers the handler reaches. Its .chains golden is checked against three independent halves of the truth: a compiler call-chain oracle (truth/cc-oracle.mjs, the TypeScript compiler API over the fixture), the Hono oracle's middleware order, and a runtime trace (truth/chain-runtime.mjs: the fixture instrumented by the repository's runtime oracle and each chain's request sent to the real pinned hono). tools/chain_check.py compares them and lists every difference.

TypeScript cases: ctx-partial-route, ctx-partial-untyped, ctx-services-typed, ctx-set-untyped, function-values, library-generic-params, post-validator-chain, same-name-unrelated, this-in-nested-arrows. JavaScript: esm-services-chain.
@swapnilpaliwal-sd swapnilpaliwal-sd changed the title typescript, javascript: Hono route table, middleware chains, context keys and request links typescript, javascript: Hono route table, middleware chains, context keys, request links and call chains (#2003) Oct 30, 2026
…a hop when the class reads the field

`constructor(private readonly onTimeout: () => void) {}` never names the
parameter in the constructor body, so the liveness guard on callback_handed
held the hand-over dead. The class runs it through the field instead:
`this.onTimeout()` in a method, or `setTimeout(this.onTimeout, ms)`. A parameter
that declares a field now counts as run wherever that field is read. A
parameter property that nothing reads stays dead, as before.
…er property

87-handed-parameter-property: the field is invoked in a method (Watchdog) or
handed on to a timer (Ticker), and both hand-overs are callback_handed hops.
The controls are a parameter property no member reads (Silent) and a plain
constructor parameter the body never reads (Ignores); neither gets a hop.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engine Resolution / call-graph engine rules enhancement New feature or request javascript JavaScript typescript TypeScript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant