feat!: use tagged JSON end to end - #131
Conversation
|
test: build lone surrogates at runtime in hydration table — Fixes the Ubuntu coverage step (run 34596435636), where every test passed and then Deno 2.9.6 panicked at Evidence (Deno 2.9.6 locally, CI's exact
|
|
Security review at
Merge blocker unrelated to security: the Ubuntu job's coverage collection crashes in Deno ( Adoption in udibo/udibo owes threat-model updates: T10's "cannot break out because the payload is base64" and T9's "not a demonstrated request-controlled injection path" must be rewritten now that loader data is readable text protected by escaping. |
|
Founder decision: do not merge this as scoped. The framework should have exactly one encoding and decoding approach across HTML hydration, data requests and streaming. A partial switch leaves two formats and two decode paths to keep in agreement, which is where bugs and conflicts come from — juniper#132's Three constraints for whatever lands instead:
Also being assessed before any decision: whether this PR's tagged-JSON scheme is as secure as what it replaces. superjson was originally chosen partly on its claim of doing this safely and efficiently, and nobody has yet judged the Measurements from the udibo side, for the record (brotli q5, 19 real payloads): compressed wire size is a wash — tagged JSON wins 2.6-10.8% above roughly 2.5KB of CBOR and loses up to 39.6% below it, largest absolute win 505 bytes. The real number is the client bundle: cbor2 is 12,844 bytes brotli, about 11.5% of eagerly loaded JavaScript, and it only leaves the browser if the data path moves too. Full tables: https://github.com/udibo/udibo/issues/974#issuecomment-5647170496 🤖 Generated with Claude Code |
The document's hydration payload is now version 3: loader data travels as
JSON text with values JSON cannot carry written as {"$t": tag, "v": value}
objects, instead of base64 of CBOR. Base64 is a third larger and hides the
page's text from the compressor; a prose page drops from ~2.5x to ~1.4x its
own markup in brotli. Data requests keep CBOR and streaming.
The client still decodes version 2, so documents cached before an upgrade
hydrate with the new bundle.
Refs udibo/udibo#974
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deno 2.9.6 panics collecting coverage for a module that has an
anonymous class expression followed by an object literal whose key
is a lone-surrogate escape ("key\uD800"): V8's takePreciseCoverage
response carries an escape serde_json rejects ("unexpected end of
hex escape"), so `deno task test --coverage` dies after every test
passed. Build the surrogates with String.fromCharCode instead; the
row round-trips the same key and value as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BREAKING CHANGE: hydration, data requests, and deferred streams use tagged JSON; legacy hydration versions trigger guarded reloads.
d821733 to
9ff4692
Compare
|
feat!: use tagged JSON end to end — Rebased onto current main and widened this PR to the complete 0.12.0 switch. All transports use tagged JSON, deferred data remains progressive, cbor2 is removed, and the public registration shapes are unchanged. Review fixes and 34 mutation runs are included; local checks and all 480 test steps pass. The PR body records the unchanged raw documentation-lint gate and rollout work still pending. |
|
Updated the documentation in 8c6dac0: removed the README's 0.12 migration notice and rewrote the hydration guide to describe current behavior without version numbers or legacy-decoder language. The remaining guides already describe current behavior directly. Validation: |
# [0.12.0](0.11.6...0.12.0) (2026-09-13) * feat!: use tagged JSON end to end ([#131](#131)) ([f873721](f873721)), closes [udibo/udibo#974](https://github.com/udibo/udibo/issues/974) ### BREAKING CHANGES * hydration, data requests, and deferred streams use tagged JSON; legacy hydration versions trigger guarded reloads. * fix: cover tagged JSON review edge cases * docs: describe current serialization behavior * fix: keep private helpers out of API docs
|
🎉 This PR is included in version 0.12.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary
Juniper now uses tagged JSON text for HTML hydration, settled client data, and streamed deferred data. This widens the existing hydration PR into the complete breaking minor switch requested for 0.12.0.
Changes
Testing
Documentation follow-up 0293095: package checks and 43 suites/480 steps pass; the six documentation-gate regressions pass, with red/green evidence and separate mutations for both Builder helpers. CI for this head is pending.
CI at 9ff4692: macOS, Windows, Ubuntu, formatting/lint, PostgreSQL template, coverage, PR title, and preview build all passed. CI run. Release is skipped until merge.
deno task check: passed, including configured documentation lint and JSDoc examples for all nine exports.
deno task test --parallel --reporter=dot: 43 suites, 480 steps passed with sanitizers enabled.
Production example build passed. Eager JS (static imports, each file Brotli quality 11): 103,824 B on released 0.11.6 -> 92,298 B, saving 11,526 B (11.1%). Raw JS: 366399 -> 326649 B. No cbor2 reference remains in src or deno.lock.
Four independent review perspectives completed. Findings fixed with red/green regressions: Unknown-name error collision; application registrations corrupting environment/diagnostic metadata; nested promises orphaned by failed deferred encoding; example copy conflating document HTML streaming with NDJSON navigation data.
Remaining gates and rollout
Literal deno doc --lint remains red with 23 npm cross-package type-reference diagnostics. The two Juniper missing-JSDoc diagnostics are fixed in 0293095 by using private identifiers for Builder helpers, and their former gate exemption is removed. Deno 2.9.6's cross-package filter retains every reference whose URL starts with file:, which includes resolved npm declarations. A standalone file importing only OpenTelemetry Span reproduces this outside Juniper. Exact aliases and dependency reexports do not solve it; no public API facade, new suppression, or widened allowlist was introduced. The remaining raw-clean gate requires an upstream Deno fix; the configured package documentation gate and examples pass.
The own-key production fix is merged as #133. The reported macOS timing failure was separately fixed in #134, which is merged; main released 0.11.6. This PR has not been merged. Udibo/website adoption, preview docs content-encoding: br, and pre-deploy-tab verification await founder merge and JSR 0.12.0. No adoption PR is open yet. The framework preview build succeeded, but its console link requires sign-in in the available browser, so a live preview stream check remains unverified. Local progressive streaming and cancellation tests passed.
Compatibility
BREAKING CHANGE: All Juniper payloads use tagged JSON; old hydration versions reload instead of decoding. All bigint values retain bigint type. Registered serializer output is recursive, unknown names throw, and error envelopes separate __errorType from data. Non-Error failures use a null error type so every string name remains available for registration. Server and browser builds must be deployed together. The 0.x breaking-release rule selects minor, yielding 0.12.0 from 0.11.6.
Red/green and mutation evidence
Initial focused tests against the original hydration-only implementation:
deno task test --reporter=dot _serialization_wire.test.tsfailed at settled-text type, small bigint (0 vs 0n), recursive serializer Promise vs Object, self-matching serializer, error-envelope name, and publicEnv sibling assertions. Its unknown-registration/stream failures were protocol mismatches and are not counted; the dedicated mutations below provide their intended-assertion evidence.After fixes,
deno task test --reporter=dot _serialization_wire.test.tspassed 20 steps.deno task test --reporter=dot _serialization_review.test.tsfirst failed four intended assertions (registered Unknown restored as undefined, publicEnv returned Tagged instead of production, missing diagnostic log, stream never closed), then passed with the review fixes. The final full suite passed all 480 steps.tag-key-escape
Command:
deno task test --reporter=dot _serialization_hydration.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).own-proto
Command:
deno task test --reporter=dot _serialization.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).script-escape-0
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).script-escape-1
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).script-escape-2
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).unknown-tag
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).small-bigint
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).unknown-type
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).unknown-error
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).stream-failure-isolation
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).recursive-type
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).recursive-stream-type
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).self-type
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).self-error
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).error-envelope
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).first-registration
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).back-pressure
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).abort-cleanup
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).response-marker
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).content-length
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).no-transform
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).registration-sorting
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).registration-diagnostics
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).hydration-version
Command:
deno task test --reporter=dot _client_wire.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).http-error-exposure
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).abort-rejection
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).predicate-order
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).settled-content-type
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).client-data-dispatch
Command:
deno task test --reporter=dot _client_wire.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).client-api-marker
Command:
deno task test --reporter=dot _client_wire.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).recursive-error
Command:
deno task test --reporter=dot _serialization_wire.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).diagnostic-error-names
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).diagnostic-context-names
Command:
deno task test --reporter=dot _server_hydration.test.tsx(only the relevant existing test/describe selected; mutation applied alone and restored).unknown-sentinel-fail-closed
Command:
deno task test --reporter=dot _serialization_review.test.ts(only the relevant existing test/describe selected; mutation applied alone and restored).All 34 focused mutations exited nonzero at behavioral assertions. The $t escape, defineOwnValue replacement, and each of < / U+2028 / U+2029 escaping were mutated independently; script assertions inspect rendered HTML. The proto test installs the browser accessor and verifies hydration, data requests, initial stream, and deferred stream in both encoder/decoder modes.
Closes
Nothing in this repository. udibo/udibo#974 stays open until adoption and rollout validation are complete.