Runtime Changelog & Coverage
What changed in pp-reactive-v2.min.js since the docs were last revised, what an
existing backend integration should add, where each runtime feature is documented,
and the known gaps in the runtime itself. Everything here was checked against the
runtime's TypeScript source and test suite, not inferred from older docs.
What changed
| Date | Change | Affects | Docs |
|---|---|---|---|
| 2026-09-21 | pp.socket stays connected: a {"__pp": "ping"} heartbeat every 25 s, automatic reconnect with backoff after unexpected closes (not after close(), an error frame, 1000 or policy codes), sends buffered while reconnecting, and new onReconnecting, onOpen({ reconnected }) and onClose({ willReconnect }). Servers must answer the ping and close idle peers with 4000. | Component authors, backends with sockets | Staying connected |
| 2026-09-15 | pp.rpc rejects with a typed RpcError exposing status, errors (field messages), requestId and body. message is unchanged. | Component authors, backends | RPC errors |
| 2026-09-15 | SPA navigation replaces every <head> element marked data-pp-meta (description, canonical, Open Graph, Twitter) with the next page's set. | Backends | Navigation contract |
| 2026-08-31 | A server layout may wrap its page as <template pp-component="layout_x"> whose root is the page's own boundary (a different id). Both identities are kept; the layout gets a display: contents element of its own. | Backends with nested layouts | Layouts |
| 2026-08-28 | Loops inside slot content (children passed into a component) no longer share the owner's row cache; fixes a stray <li pp-keep-run> and a 'Loop row reuse/patching was abandoned' warning. | Component authors | Components |
| 2026-08-22 | A controlled checked is applied to the element's property in both directions, including keyed pp-for rows, so state false now unticks a box the user ticked. | Component authors | Forms |
| 2026-08-22 | Hyphenated attributes that only contain a boolean name (aria-disabled, data-open, aria-checked) are ordinary string attributes and render "true"/"false". | Component authors | Forms |
| 2026-08-22 | SPA navigation no longer hides the body during a swap. The swap and hydration happen in one task, and scroll is corrected before the first paint. | Everyone | Scroll restoration |
| 2026-08-21 | pp-reset-scroll="true" panes reset even when their scroll key changed between pages; a cross-page #hash is applied after the reset. | Everyone | Scroll restoration |
| 2026-08-18 | Keyed rows keep being patched after they are portaled out of the component's root (dropdowns, menus). | Component authors | Portals |
| 2026-08-15 | An uncontrolled <textarea> keeps what the user typed across re-renders; a textarea becomes controlled once its bound content changes. | Component authors | Forms |
| 2026-08-14 – 08-16 | Internal speedups: component body plans, direct row construction, fused child mounts, flyweight rows. No API change. | Nobody (faster) | Performance |
| 2026-08-11 | HTML comments authored in templates are stripped before compiling (except inside <pre>/<code>), so a {…} inside a comment is never evaluated. | Component authors | Directives |
What an existing backend integration should add
- If you serve named sockets, answer {"__pp": "ping"} with {"__pp": "pong"} (never pass it to the handler) and close idle peers with 4000, not 1000. Without the pong the client replaces the connection about every 45 s.
- Mark per-page <head> tags (description, canonical, robots, og:*, twitter:*) with data-pp-meta, or they stay frozen on the first page after an SPA navigation.
- Send X-PP-Root-Layout and render <meta name="pp-root-layout" content="…"> for each group of pages with its own <head> assets. Both are needed for the check to run.
- Return validation failures as a 4xx with { "error": "…", "errors": { field: ["…"] } }, and optionally a "requestId".
- Return JSON from every RPC, including null for a function with no result. The client parses every 2xx body.
- Keep the pp_csrf cookie readable by JavaScript (not HttpOnly).
- If your layouts wrap pages in their own <template pp-component>, nothing extra is needed any more; ids no longer have to match.
None of these break an older integration; each one makes a feature work that did not before. The full contract, with reference code in four languages, is in Implement In Your Backend.
Where every feature is documented
| Runtime surface | Page |
|---|---|
| Template syntax, directives, handler identifiers | Directives & Public API |
| pp.state, pp.reducer | State |
| pp.effect | Effect |
| pp.ref, callback refs | Ref |
| pp-for, keys, per-row reuse | Loops & Keyed Lists |
| Boundaries, props, children, composition, fragments, context | Components |
| memo, callback, layoutEffect, id, syncExternalStore, imperativeHandle, transition, deferredValue, optimistic, errorBoundary | Advanced Hooks |
| Controlled/uncontrolled fields, checkbox, select, textarea, reset | Forms |
| pp.portal | Portals |
| pp.rpc options, results, RpcError, races, uploads, streaming, CSRF | RPC, Errors & Uploads |
| pp.mount, pp.redirect, link interception, head sync, scroll, loading UI, navigation events | SPA Navigation |
| pp.socket | WebSockets |
| pp.enablePerf, getPerfStats, console messages | Performance & Debugging |
| ComponentInit.bootstrap vs pp.mount, static pages | Plain HTML |
| The complete server contract | Implement In Your Backend |
| Machine-readable summary of all of the above | llms.md |
Known gaps in the runtime
These are how the current runtime behaves, not documentation omissions. Each has a workaround that works today. They are candidates for a future runtime release.
| Gap | Impact | Workaround |
|---|---|---|
| pp-loading-transition does not fade | fadeIn/fadeOut set a CSS transition and wait, but the runtime never changes opacity, so they only delay the swap. | Add your own opacity rule to the loading markup, or treat the values as delays. |
| abortPrevious is page-wide | One cancel slot is shared by every pp.rpc call, so two independent search boxes cancel each other. | Use a generation counter in pp.ref for independent lists. |
| onStreamError catches every failure | When set, HTTP and network errors also go to it, and the promise resolves undefined instead of rejecting. | Only set it on streaming calls, and branch on error.status inside it. |
| SSE events are line-based | Each data: line is its own chunk; multi-line data fields are not joined, and event:/id: fields are ignored. | Send one JSON value per data: line. |
| No head asset loading on SPA navigation | New stylesheets or scripts in the next page's <head> are not loaded, and plain <script> tags in the new body do not run. | Use X-PP-Root-Layout to force a full load between sections, and keep page behavior in component scripts. |
| Numeric pp.optimistic bases cannot be rolled back | Pending guesses clear only when the base changes identity, so a failed call on a primitive base leaves the guess shown. | Keep the base in an object and set a fresh copy on failure. |
| Fixed loading container id | Loading UI is looked up under the hard-coded id loading-file-1B87E. | Render that exact id; it is part of the contract. |