Repository navigation
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
Open
swapnilpaliwal-sd wants to merge 9 commits into
swapnilpaliwal-sd wants to merge 9 commits into
Conversation
swapnilpaliwal-sd
force-pushed
the
release/hono-0.2.3
branch
from
October 29, 2026 16:41
8774f52 to
176be44
Compare
…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
marked this pull request as ready for review
October 30, 2026 02:09
swapnilpaliwal-sd
requested review from
JaredHLZhang,
Whua689 and
suyashpaliwal26
as code owners
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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2003
Merges into the
0.2.3release branch, which then goes todev; not proposed for merge before the 0.2.3 release.Before merge (open for review):
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:
on,allandroute()registration becomes aroute with its composed path, through
basePath, nested mounts, sub-apps built by factories orregistration 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 abasePathresult. A path piece that is not a literal or a const is reported as undeterminedwith a reason, never guessed.
(app-wide and path-scoped
use), in registration order, tagged when a registration sits insidea branch or a loop.
c.set(key)in a middleware links to thec.get(key)/c.var.key/const { key } = c.varreads in the handlers it runs before; a set made afterawait next()does not.
hcclient call, afetchto a path the programspells, and
app.request(..)in a test link to the route they reach, with the method taken fromthe request. A request whose method or base cannot be decided says so instead of joining at
random.
through
c.req.valid(..).(
const { orders } = c.get(key), thenorders.create(..)), element types of arrays and tables,thisinside arrow functions, function values handed to a callee that runs them (a newcallback_handededge tier, ranked as a hand-off), and decided table keys. A function handedto code that never runs it (a getter read, an unread or only-compared parameter, a keyed
registry slot) gets no edge.
impact,contextandpathanswer on all of these with existing options only.Placement: the framework rules live in each engine's
config-resolution/andframework-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.3Against a clean checkout of
0.2.3(which includes #2002) run the same way.0.2.3graph/test/typescript/run-tests.sh, preflight incl. compiler-load lint)tests/run.py --lang typescripttests/run.py --lang javascript--oracle, bare and library variants)--oracle)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.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 from0/413 to 413/413, middleware -> handler hops from 0/2488 to 2488/2488, and
contextanswers on arequest 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.
every project inside its noise band; peak RSS within +28 MB; every graph grew.
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).
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
0.2.3branch carries the 0.2.3 version change.Merge conditions
dev(table above), with chain-edge precision not fallingand no new false chains (see "Before merge" on how that is counted).