Add declarative global-data subscriptions - #294
Conversation
Coverage Report for CI Build 34160465524Coverage increased (+0.02%) to 95.329%Details
Uncovered Changes
Coverage Regressions4 previously-covered lines in 2 files lost coverage.
Coverage Stats💛 - Coveralls |
|
This is really complex and ugly code. Does it make sense to have layout data or page data instead and then we just rebuild layouts and pages based on that relation. |
|
Reworked in 915501d around that boundary. Only global.data receives the source PageData collection now. Pages and layouts declare top-level keys in vars.dataDependencies, templates and page factories use a named dataDependencies export, and every consumer receives only those values through a separate data argument. This removes AsyncLocalStorage, render-context attribution, page-property observation, and the page-collection dependency graph. Targeted builds now fingerprint top-level global-data keys and execute only subscribed pages, templates, and generated-page owners. The resolved-layout and obsolete-output fixes remain intact. |
3a74b7f to
65881c6
Compare
1141daf to
2eec023
Compare
2eec023 to
36ffe87
Compare
There was a problem hiding this comment.
🟡 Changes recommended
A few small but concrete issues remain in changed code (redundant/incorrect type fixtures and an inconsistency that can leak absolute paths in conflict errors).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces a declarative global-data subscription boundary so only global.data.* receives source-backed PageData[], while pages/layouts/templates/factories explicitly declare top-level keys they need and receive them via a separate data argument.
It also adds watch-mode fingerprinting for top-level global-data values so incremental rebuilds target only subscribers of changed keys, plus preserves data-subscription errors across the worker boundary.
Changes:
- Add
dataDepsdeclaration +dataparameter plumbing across page/layout/template/generated-page factory execution and types. - Track subscriptions + fingerprints in watch mode and invalidate only subscribed consumers for changed global-data keys.
- Add
DomStackDataError(DOM_STACK_ERROR_DATA) and serialize/restore domain error metadata across worker transport.
File summaries
| File | Description |
|---|---|
| types.ts | Re-exports new public types (DataDeps, AsyncTemplateFunction) to support subscription typing. |
| test-cases/watch/index.test.js | Adds/updates watch-mode tests for data subscriptions, invalidation, collisions, and failure recovery. |
| test-cases/type-exports/index.test.ts | Updates type-export fixtures to use data instead of pages. |
| test-cases/type-exports/data-deps.test.ts | Adds compile-time tests for DataDeps and independent producer/consumer data contracts. |
| test-cases/nested-layouts/types.test.ts | Updates nested-layout type fixtures for data generics and new render helper signatures. |
| test-cases/nested-layouts/index.test.js | Adds runtime tests for nested-layout subscriptions, projection isolation, and watch invalidation behavior. |
| test-cases/generated-pages/src/summary.template.js | Converts template to declarative dataDeps + data consumption. |
| test-cases/generated-pages/src/root.layout.js | Declares layout dataDeps and reads subscribed data via data. |
| test-cases/generated-pages/src/redirects.pages.js | Converts generated-pages factory to named dataDeps and data input. |
| test-cases/generated-pages/src/indexes.pages.js | Converts generated-pages factory to named dataDeps and data input. |
| test-cases/generated-pages/src/concrete-only.pages.js | Removes raw page-collection introspection from factory; uses subscribed data instead. |
| test-cases/generated-pages/src/blog-index.layout.js | Declares layout dataDeps and updates layout typing for subscribed data. |
| test-cases/generated-pages/index.test.js | Updates generated-pages tests for subscription model and adds worker error round-trip assertions. |
| test-cases/general-features/src/templates/single-object.template.js | Updates template typing to async template type export. |
| test-cases/general-features/src/templates/simple.txt.template.js | Updates template typing to async template type export. |
| test-cases/general-features/src/templates/object-array.template.js | Updates template typing to async template type export. |
| test-cases/general-features/src/README.md | Demonstrates frontmatter dataDeps and data.* template usage. |
| test-cases/general-features/src/global.data.js | Expands global-data outputs (years/feed items) and shifts collection work into global data. |
| test-cases/general-features/src/feeds.template.js | Converts feed template to named dataDeps and consumes prepared feed records. |
| test-cases/general-features/src/blog/page.js | Converts blog index page to subscribed data input and adds dataDeps. |
| README.md | Documents new subscription boundary, updated signatures/types, watch behavior, and migration guidance. |
| plans/v12-release-review.md | Adds/updates release-review notes to reflect resolved issues and new subscription model. |
| plans/generated-pages.md | Notes that earlier “pages/vars stamping” behavior is superseded by subscriptions. |
| lib/helpers/domstack-error.js | Introduces DomStackDataError with structured dataDependency metadata. |
| lib/build-pages/worker.js | Ensures worker reports serialized build errors instead of throwing them raw. |
| lib/build-pages/watch-dependencies.test.js | Adds unit tests for subscription extraction, projection, fingerprinting, and invalidation. |
| lib/build-pages/watch-dependencies.js | Implements subscription tracking + top-level fingerprint comparisons for watch invalidation. |
| lib/build-pages/resolve-vars.js | Updates global-data resolution typing and comments for subscription model. |
| lib/build-pages/page-data.test.js | Updates PageData tests for vars layering changes and new data-deps/data behavior. |
| lib/build-pages/page-data.js | Implements page/layout subscription extraction, data projection, and render helper signature changes. |
| lib/build-pages/page-builders/template-builder.test.js | Updates template builder test harness for new builder inputs. |
| lib/build-pages/page-builders/template-builder.js | Adds template dataDeps support and passes subscribed data to templates. |
| lib/build-pages/page-builders/page-writer.js | Removes pages plumbing; relies on PageData’s internal state and subscribed data. |
| lib/build-pages/index.js | Threads subscriptions/fingerprints through build pipeline; adds error serialization; targets invalidated subscribers. |
| lib/build-pages/data-deps.js | Adds runtime declaration validation, extraction, and projection guard proxy for subscribed data. |
| index.js | Integrates watch dependency state into incremental rebuild routing and global-data helper import tracking. |
| examples/uhtml-isomorphic/src/layouts/root.layout.js | Updates layout signature comment for data instead of pages. |
| examples/preact-isomorphic/src/layouts/root.layout.ts | Updates example layout props to accept data instead of pages. |
| examples/blog/src/redirects.pages.ts | Converts example generated-pages factory to dataDeps + subscribed data. |
| examples/blog/src/README.md | Converts example markdown to frontmatter dataDeps + data.* usage. |
| examples/blog/src/layouts/root.layout.ts | Removes global-data type merge from vars type to match new boundary. |
| examples/blog/src/global.data.ts | Adds explicit source input typing and exposes focused consumer contracts for subscriptions. |
| examples/blog/src/feeds.template.ts | Converts example feed template to subscribe to prepared feed items via data. |
| examples/blog/src/blog/page.ts | Converts example blog index to subscribe to blog collections via data. |
| examples/blog/src/blog/2024/hello-world/README.md | Updates docs to describe subscription model instead of global data stamped into vars. |
| examples/blog/src/blog-indexes.pages.ts | Converts example year-index generator to dataDeps + subscribed data. |
| docs/v12-migration.md | Updates migration guide for subscriptions, removed pages plumbing, and error behavior. |
Review details
Suppressed comments (1)
test-cases/general-features/src/blog/page.js:8
- The page is annotated as returning
string, but it returns afragtmlHtmlResult; this forces a@ts-ignorebelow and undermines the type test fixture.
/**
* @type {AsyncPageFunction<{}, string, { blogYears: string[] }>}
*/
- Files reviewed: 47/47 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
36ffe87 to
dd0a049
Compare
b198428 to
b380ff6
Compare
Planning and release-review contextMoved the new release-review document and generated-pages plan note out of this PR's source changes. These notes preserve the review history; statements about the published beta, validation, and remaining findings describe the review snapshot, not a fresh release-readiness assessment. Related issues
Generated-pages plan noteThe existing Full release-review notes, relocated from plans/v12-release-review.mdDOMStack v12 release reviewThis document records the issues found while reviewing Release blockersResolved: watch builds can leave collection consumers staleA targeted source-page rebuild recalculates This was reproduced with two Markdown pages and a The prerelease API allowed these dependency paths:
The resolved API removes raw page collections from ordinary consumers and routes intentional collection processing through Blanket rebuilding every consumer would be correct but would defeat the purpose of granular rebuilds. The branch now resolves this through the explicit subscription model described in the "Declarative global-data dependencies" section below. Resolved: layout watch mapping ignores builder vars and Markdown frontmatterAt the time of the review, the watcher's page-to-layout map resolved only default, global, and This was reproduced with a Markdown page that selects The nested-layout prerequisite now reports each page's fully resolved Published declarations do not pass strict consumer validationThe published The DOMStack-owned declaration errors are emitted for The public declaration graph also exposes declaration errors from The repository's Other findingsOffline examples extend a nonexistent TypeScript configurationBoth new offline examples extend The example build exits successfully while esbuild reports the missing configuration as a warning. This means the examples currently build without their intended shared compiler settings. Affected files:
Production dependency audit reports a high-severity advisory
The advisory concerns stack exhaustion while merging recursive object graphs, so exposure through parsed Declarative global-data dependenciesThe original experimental implementation inferred output dependencies by wrapping page collections and vars in read-tracking proxies, then used async-local context to attribute reads during concurrent rendering. That was mechanically capable but preserved the wrong public boundary: every consumer still received the complete page graph and therefore remained a potential collection consumer. The replacement design makes collection processing an explicit phase:
The worker fingerprints every top-level global-data value during watch page builds. It compares those fingerprints with the previous successful watch state and invalidates only consumers subscribed to keys whose values changed. Subscriptions are explicit records keyed by source page, generated output, template, or pages-file owner, so no async attribution or property-read graph is required. Output dependencies are the union of declarations from frontmatter or page vars and every layout in the resolved This model intentionally tracks at top-level global-data key granularity. A consumer of Manual composition remains supported and tested, including subscribed data and statically imported parents. Explicit Validation completed during review
|
1ff541a to
e8e9e85
Compare
Summary
global.data.*the only public hook that receives source-backedPageData[].vars.dataDeps.*.pages.*factories declare keys through a nameddataDepsexport.dataargument instead of merging them intovars.DataDeps<Contract>.Stack
Base: #311 (
bret/nested-layouts), which introduces explicit nested layouts and resolves #290. This PR adds global-data subscriptions on top. The separate watch-maintenance stack is unchanged.Design
This replaces the runtime property-observation design with an explicit data boundary.
global.data.*owns all source-page collection work and returns named values such asrecentPosts,blogIndexes, orfeedItems. Ordinary pages, layouts, templates, and page factories do not receive raw page collections. They declare the top-level values they need and receive only those values throughdata.Pages declare dependencies in frontmatter, adjacent page vars, or a TypeScript page's
varsexport. Layouts declare dependencies in theirvarsexport. Each renderer receives only its own declared data. DOMStack unions page declarations with every layout in the resolvedparentLayoutchain for output invalidation, then removesdataDepsfrom ordinary vars. Ancestor subscriptions are inherited for invalidation without being repeated by children or leaking into another renderer’s data. While global data is resolving,renderInnerPage()remains available for pages without their own subscriptions, even when their layouts subscribe.renderFullPage()requires the whole chain to be unsubscribed at that stage. The TypeScript examples export focused contracts such asFeedsTemplateDatafromglobal.data.ts, so consumers do not reconstruct selections from the completeGlobalDatatype.DataDeps<Contract>supports readonly declarations and checks names against the local consumer contract.PagesFunction<PageVars, Content, FactoryVars, FactoryData, PageData>separates a factory's input from its inline pages' subscriptions.GlobalDataFunction<Result, SourceVars, SourceContent>types the producer's source-page input without downstream casts.Manual function composition is supported and tested, including subscribed data, generated pages, assets, and imported-parent watch updates. A composing layout declares the keys needed by the functions it calls and forwards their arguments. The README and migration guide recommend
parentLayoutso DOMStack can manage the full ancestor dependency chain automatically.Templates and
*.pages.*modules use a named export because they do not have consumer vars:Watch state now contains only consumer subscriptions and fingerprints for top-level global-data values. There is no
AsyncLocalStorage, render-context attribution, page-property graph, or observational tracking proxy. The small proxy arounddatais only an API guard that reports reads of existing but undeclared keys.JSON-safe values receive stable fingerprints. Opaque or cyclic values conservatively invalidate their subscribers on each page build rather than risking stale output. The fingerprint checks cover sparse arrays with extra properties, accessors, negative zero, and prototype-like key names without executing getters. Changes to statically imported global-data helpers recompute data while also selecting any direct consumers of the same helper, including modules that also serve as browser entry points. Untouched generated-page owner conflicts use source-relative names consistently with other output claims. The previous successful watch state remains authoritative when a build fails. The next page build retries the complete page phase, then resumes incremental routing. Subscription failures use
DomStackDataError(DOM_STACK_ERROR_DATA) with a reason, consumer, and optional key; the subtype and metadata survive the worker boundary. Declaration validation and projection live indata-deps.js, separate from watch-state bookkeeping.v12 prerelease API changes
vars.pages.siteData.PageData.renderInnerPage()andPageData.renderFullPage()no longer take a pages argument.datashape.AsyncTemplateFunctionexplicitly requires a promise.Validation
Review context
The planning and release-review notes are preserved in the PR discussion rather than tracked source files. The README and v12 migration guide remain the user-facing documentation.
Follow-ups and limitations
Fixes #289.