Skip to content

Stop the endless page walk and build the state schema faster - #474

Draft
alexluckett wants to merge 3 commits into
mainfrom
fix/page-walk-cycle
Draft

alexluckett wants to merge 3 commits into
mainfrom
fix/page-walk-cycle

Conversation

@alexluckett

@alexluckett alexluckett commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Proposed change

This fixes two problems in getFormContext, which could block or slow the main node thread.

Problem 1: The page walk may never end.

  • A form with a page at the path / made the walk loop with no end. This occurred when a user requested a page that was off the route.
  • findPage now finds no page when it gets no path. Thus the walk stops after the last page.
  • The walk now throws a PageRouteCycleError when the route comes back to a page. This also stops loops from paths such as /name and /name/, and from V1 next links that point back.

Problem 2: The state schema was slow to build.

  • makeFilteredSchema added one page at a time to one large schema. Joi copies the schema on each step, so large forms were slow.
  • It now joins the pages in batches of 32, then joins the batches.
  • The result is the same: same keys, same rules, same order of errors.

Performance

Measured on a form with 243 pages, for a request to the summary page. The number of relevant pages changes with the answers. Each time is the middle value of 9 runs on one laptop.

Relevant pages Schema build, before Schema build, after getFormContext, before getFormContext, after
94 58 ms 12 ms 74 ms 29 ms
130 150 ms 22 ms 164 ms 39 ms
174 338 ms 32 ms 355 ms 49 ms
239 865 ms 60 ms 874 ms 78 ms

The summary page builds the schema two times: one time in getFormContext and one time in SummaryViewModel.

A request for a page that is off the route, on the same form with its page at /: before, the request had no end. After, getFormContext takes 16 ms.

Jira ticket:

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Misc. (documentation, build updates, etc)

Checklist

  • You have executed this code locally and it performs as expected.
  • You have added tests to verify your code works.
  • You have added code comments and JSDoc, where appropriate.
  • There is no commented-out code.
  • You have added developer docs in README.md and docs/* (where appropriate, e.g. new features).
  • The tests are passing (npm run test).
  • The linting checks are passing (npm run lint).
  • The code has been formatted (npm run format).

getFormContext walks the pages from the start page until it gets to the
requested page. After the last page the next path is undefined, and
findPage read that as the root path '/'. A form with a page at '/' then
continued the walk from that page with no end, for each request to a page
that was off the route. The walk is synchronous, so the request blocked
the process.

findPage now finds no page when it gets no path, so the walk ends after
the last page.

The walk also remembers the pages it has visited and throws a
PageRouteCycleError when the route returns to one of them. This covers
page paths that are equal after normalisation, such as '/name' and
'/name/', and V1 next links that point back to an earlier page.
makeFilteredSchema joined the state schema of each relevant page onto one
growing schema. Joi copies both schemas on each concat, so the work grew
with the square of the page count. For a form with 239 relevant pages the
build took about 650 ms of synchronous work, on each request late in the
journey and two times on the summary page.

The schemas are now joined in pairs, then the results in pairs, until one
is left. Each key is copied about log2(pages) times. The same form builds
in about 28 ms.

It still uses concat, so object-level rules such as the country rule of
EastingNorthingField stay in the joined schema, and the keys keep the
page order.
The pairs loop made the build fast but was hard to read. makeFilteredSchema
now joins the page schemas in batches of 32, then joins the batch schemas.
Both steps use the same plain helper, joinSchemas, which adds one schema at
a time.

Inside a batch the growing schema has 32 pages at most, and only the few
joins between batches work on a large schema. For a form with 239 relevant
pages the build takes about 45 to 65 ms, against about 650 to 870 ms for
one join for each page on the full schema.

The pages are still joined first to last, so the keys, the object-level
rules and the order of errors are the same. A new test covers a form with
more pages than one batch.
@sonarqubecloud

sonarqubecloud Bot commented Oct 5, 2026

Copy link
Copy Markdown

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant