diff --git a/.changeset/pre.json b/.changeset/pre.json index ff321db92..f359d791a 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -24,8 +24,10 @@ "effect-example": "0.0.0" }, "changesets": [ + "a28-writes-visible-at-flush", "absorb-expressions", "accept-empty-server-function-posts", + "action-await-yield-docs", "action-done-window-batch-adoption", "action-in-owned-scope-guard", "action-thenable-assimilation-settle", @@ -56,11 +58,15 @@ "arch-tier1-affects-reveal", "arch-tier2-consolidations", "async-arg-border-typing", + "async-landing-deps", "async-slot-args-value-tier", + "async-store-setter-diagnostic", "async-waterfall-attribution", "attribution-feedback-facts", "attribution-feedback-tables", + "attribution-folds-named-exports", "attribution-navigation-origin", + "attribution-serializable-reruns", "attribution-unstable-memo-output", "attribution-wide-write", "attribution-write-provenance", @@ -71,6 +77,8 @@ "behavior-claims-delegate-glue", "behavior-claims-props", "bind-get-grants-to-function-identity", + "body-end-visibility", + "born-held", "bound-body-by-bytes-received", "bound-server-function-requests", "boundary-accessor-return-types", @@ -92,6 +100,7 @@ "clarify-invoke-wrapper-contract", "clean-bikes-teach", "cleanup-use-once-helpers", + "client-error-hook", "client-only-preload-hints", "client-only-sibling-id-drift", "client-store-hydration-pending", @@ -103,12 +112,14 @@ "compiler-component-names", "compiler-loader-accepts-patch-driver", "compiler-loader-prefer-local-build", + "compiler-spread-sources-array", "composed-header-bounds-transport", "contain-flight-collector-errors", "container-tier-projection-traces", "context-default-less-throws", "continue-tsrx-semantic-ir", "core-hot-path-shape-alignment", + "cross-package-owner-fields", "csr-hydration-seam-trim", "curly-rats-dress", "dedup-hydration-gate-boilerplate", @@ -117,8 +128,10 @@ "dedupe-settle-walk-code", "dedupe-single-flight-source-ids", "dedupe-strict-read-diagnostics", + "delegated-event-key-namespace", "demote-boundary-primitives", "deprecate-create-tracked-effect", + "deprecate-dynamic-component", "deprecate-server-writes", "dev-artifact-naming", "dev-error-invalid-refresh-target", @@ -149,9 +162,13 @@ "drop-compiler-legacy", "dynamic-factory-redesign", "dynamic-remount-latest-address", + "dynamic-run-hydration-events", + "dynamic-static-source", + "dynamic-xmlns-namespace", "effect-cleanup-unwind-order", "effect-error-arm-queued", "effect-error-handler-unwrap", + "effect-mainline-ownership-3412", "effect-phase-write-visibility", "effect-relay-tear", "effect-run-trims", @@ -162,6 +179,7 @@ "encrypt-flash-cookie", "enforce-provide-event-once-on-direct-calls", "enforce-provide-event-single-invocation", + "error-hook-thrower-path", "export-memo-options", "expose-enableExternalSource-loading-on", "extract-nested-server-function-declarations", @@ -183,6 +201,7 @@ "fix-array-target-dictionary-mode", "fix-asset-preload-rejection", "fix-async-boundary-diagnostic", + "fix-async-chain-second-write", "fix-async-generator-action-freeze", "fix-async-iterable-hydration-flush", "fix-async-iterator-cancellation-wrappers", @@ -242,6 +261,10 @@ "fix-frames-web-path-mapping", "fix-get-grant-lifetime-and-read-folding", "fix-halt-error-swallow", + "fix-held-conditional-effect", + "fix-held-conditional-memo", + "fix-held-reader-disposed-lane-hold", + "fix-held-transition-child-disposal", "fix-hybrid-iterable-signal-takeover", "fix-hydrated-nullish-values", "fix-hydration-and-signals-baseline", @@ -275,14 +298,18 @@ "fix-loading-boundary-stale-flatten", "fix-loading-hydration-settled-ref", "fix-loading-late-fragment-hydration", + "fix-loading-on-key-spectator-read", "fix-loading-on-prop", "fix-loading-rail-transition-reentry", "fix-loading-refresh-fallback", + "fix-loading-reset-collects-forwarded", "fix-loading-ssr-bare-async-read", "fix-loading-window-commit-zero-integrity", "fix-maparray-async-store-settle-wedge", "fix-maparray-notready-exception-safety", "fix-memo-created-inside-latest-window", + "fix-memo-pull-under-lane", + "fix-merge-plain-result-not-flattened", "fix-merged-lane-hold-per-node", "fix-mid-recompute-missed-wakes", "fix-nested-array-async-reconcile", @@ -293,17 +320,21 @@ "fix-nullish-lone-spread", "fix-omit-merge-sources-tunnel", "fix-onsettled-unowned-cleanup", + "fix-optimistic-lane-release", "fix-optimistic-override-premature-revert", "fix-optimistic-same-value-overlap", + "fix-optimistic-settle-verdicts", "fix-optimistic-store-layer-scope", "fix-optimistic-store-refetch-hold", "fix-optimistic-store-stash-flicker", "fix-optimistic-undefined-override", + "fix-overlapping-flights", "fix-overlay-adoption-reset", "fix-override-supersession-on-arrival", "fix-owned-write-guard-docs", "fix-ownerless-conditional-memos", "fix-pending-autodispose-release", + "fix-pending-companion-lane-parent", "fix-pending-gated-landing-replay", "fix-portal-mount-ref-timing", "fix-portal-ownership-and-accumulation", @@ -377,6 +408,7 @@ "fix-spread-hydration-mismatch", "fix-ssr-async-error-handling", "fix-ssr-bench-shape", + "fix-ssr-element-nullish-style-class", "fix-ssr-errored-async-retry-loop", "fix-ssr-escape-function-results", "fix-ssr-loading-errored-hydration", @@ -391,6 +423,7 @@ "fix-ssr-spread-children-read-once", "fix-ssr-stream-rejected-fragment-hang", "fix-ssr-sync-memo-child-id-leak", + "fix-ssr-text-separators-resolved", "fix-ssr-textarea-spread-value", "fix-ssrsource-client-hybrid-hydration", "fix-stateful-prop-hydration-adoption", @@ -429,6 +462,7 @@ "flash-falsy-nojs-outcomes", "flash-key-derivation", "flatten-promise-of-async-iterable", + "flight-consumers-receive-response-metadata", "fluffy-bananas-smoke", "fold-runtime-wrappers", "for-hydration-id-parity", @@ -449,7 +483,9 @@ "fuzzy-queens-retire", "gate-child-companion-walk", "gate-invariants-to-test-builds", + "gated-reporter-releases-hold", "get-read-contract", + "getter-keys-not-computed", "golf-effect-ownership-flag", "graph-size-diagnostics", "guard-error-carried-failure-channels", @@ -461,9 +497,12 @@ "heap-mark-incremental", "hide-internal-type-exports", "hoist-web-protocol-components", + "hold-consistency-batch-2", "hot-scope-fanout-aggregation", "html-braced-comments", "html-element-claims", + "hydrate-claim-path", + "hydrating-root-snapshot-scope", "hydrating-store-engine-seam", "hydration-primitive-wrappers", "identity-keyed-server-function-ids", @@ -472,8 +511,12 @@ "import-solid-signals-monorepo", "improve-tsrx-lazy-destructuring", "include-web-cookie-declarations", + "internal-view-protocol-surface", "inv2-active-override-assertion", + "ispending-before-first-commit", + "ispending-held-sync-memo", "ispending-loading-guard", + "ispending-memo-unstamped-hold", "ispending-wrapper-fresh-hold", "iterate-bare-step-tolerance", "iterator-supersede-eager-close", @@ -486,6 +529,9 @@ "late-boundary-held-fragment-wait", "latest-lazy-companion-backfill", "latest-probe-order-independence", + "latest-seed-invisibility", + "latest-shadow-owned-write", + "latest-uninitialized-throws", "lazy-assets-post-settle-livelock", "lazy-async-asset-resolver", "lazy-async-inflight-autodispose", @@ -513,6 +559,7 @@ "maparray-small-move-fast-path", "maparray-swap-commit", "mean-bats-grin", + "merge-omit-lazy-views", "migrate-web-runtime-to-ts", "missing-effect-fn-loud-failure", "mode-specific-control-flow-callbacks", @@ -525,9 +572,13 @@ "nohydration-lazy-maparray-deps", "notready-skip-stack-capture", "npm-descriptions", + "observe-exclude-interaction-writes", "observe-node-shapes", + "observe-records-channel", "observe-shape-freeze", "observe-tier", + "omit-nested-merge-record", + "one-unflushed-adopted", "optimistic-draft-reads-compose-over-staged-truth", "optimistic-landing-fold", "optimistic-lists-drivable", @@ -542,6 +593,7 @@ "own-decoder-minted-rejected-promises", "own-the-single-flight-fold-before-stamping", "owned-non-lazy-memo-autodispose", + "owns-hold", "pass-2-collapse", "patch-channel-pay-for-use", "patch-channel-pr-a", @@ -566,6 +618,7 @@ "perf-2893-guard-cleanup", "perf-dep-revalidation-and-reconcile", "perf-footer-attribution-discovery", + "perf-ssr-walker-entry", "perfect-spies-pretend", "phase2-optimistic-code-reduction", "pin-prop-condition-memo-id-parity", @@ -574,8 +627,10 @@ "portal-marker-cleanup", "predigested-flight-outcome", "preload-identity-qualifiers", + "preloadable-server-function-reads", "preserve-bound-event-tuples", "preserve-store-symbol-snapshots", + "projection-leaf-companions-die-with-firewall", "projection-leaf-release", "projection-lists-drivable", "projection-root-entity-swap", @@ -617,6 +672,7 @@ "reject-server-directive-on-methods", "reject-this-arguments-server-function-capture", "reject-unsupported-server-module-exports", + "release-companion-leaf-3503", "relocate-uninit-lane-check", "remove-capturing-event-listeners", "remove-cjs", @@ -628,11 +684,13 @@ "remove-loading-notify-through-remap", "remove-on-namespace-events", "remove-patch-channel", + "remove-server-function-instance-header", "remove-ssr-run-in-scope", "remove-store-readonly", "rename-createLoadingBoundary", "rename-pending-to-latest", "rename-purewrite-to-ownedwrite", + "render-to-string-sync-dispose", "renderer-owned-jsx-types", "repeat-disjoint-window", "republish-fresh-signals-build", @@ -655,8 +713,13 @@ "scope-function-children-holes", "scope-nested-deferred-results-without-mutation", "serialization-decode-entry", + "serve-one-slow-selection", + "server-boundary-records", "server-component-context-barrier", "server-dev-build", + "server-diagnostics-artifact", + "server-diagnostics-channel", + "server-error-hook", "server-first-value-lock", "server-function-call-observers", "server-function-data-address-split", @@ -667,13 +730,16 @@ "server-function-path-addressing", "server-function-path-names", "server-function-types-not-any", + "server-function-url-takes-the-reference", "server-functions-client-fetch", "server-memo-commit-liveness", "server-memo-scaffold-diet", "server-mock-csp-nonce", + "server-observe-install-point", "server-projection-errors", "server-slot-store-and-boundary-trims", "server-store-replacement-optimistic-noop", + "server-trace-context", "setter-row-ops-tentative-lists", "settle-superseded-flight-dependents", "settle-unchanged-pending-branches", @@ -683,9 +749,11 @@ "sever-reveal-group-at-boundaries", "shallow-compiled-slot-channel", "shallow-slot-alignment-samevaluezero", + "shallow-store-leaves-stay-raw", "shallow-stores-markraw", "share-server-function-registry-across-reloads", "shared-effect-status-notifier", + "shared-read-predicates", "shell-gate-first-apply", "signals-dev-cjs", "signals-disposed-stay-disposed", @@ -702,24 +770,38 @@ "snapshot-hydration-safety", "snapshot-trailing-holes", "solid-native-frame-protocol-identifiers", + "spec-a33-fallback-hold", "spread-direct-sources", + "spread-fewer-nodes", + "ssr-boundary-resolve-scope-3414", + "ssr-component-names", + "ssr-element-join-in-place", + "ssr-element-sources", + "ssr-error-sanitization", "ssr-falsy-rejections-and-thenables", "ssr-hole-id-scopes", "ssr-retry-robustness", "ssrSource-effects-and-fix-server-createEffect", "ssrsource-async-iterables", + "staged-reporter-deps-tail", "stale-read-uninitialized-cross-transition", "steady-reveal-composites", "store-applystate-fast-slow-split", + "store-backing-stale-replay", "store-copy-own-helper", "store-deep-witness-node", "store-descriptor-override-value", + "store-descriptor-trap-subscribes", "store-first-read-diet", + "store-hold-visible", "store-hot-path-trims", "store-next-settled-visibility", + "store-node-rule1", + "store-override-survives-unobserved", "store-owner-stamp", "store-pending-overlay", "store-platform-objects-raw", + "store-presence-override-survives-unobserved", "store-rewrite-creation-perf", "store-rewrite-delete-legacy", "store-rewrite-derived-form", @@ -737,14 +819,18 @@ "store-rewrite-suite-green", "store-rewrite-tentative-reconcile", "store-rewrite-walk-validation-hoist", + "store-setter-guard-no-root-exemption", "store-structural-hot-path-pass", "store-thenable-probe-strict-read", "store-treeshakeable-optimism", + "store-untracked-born-held", "store-write-floor-narrow", "stream-demand-gate", + "stream-error-drops-settled-children", "strict-untrack-warning", "strictread-untrack-parameter", "strip-proto-key-at-argument-decode", + "superseded-source-blocks", "support-async-memo-callbacks-ssr", "support-delegated-event-listener-objects", "support-tsrx-expression-containers", @@ -764,10 +850,12 @@ "transparent-effect-option-typed", "transport-failure-on-foreign-response", "truncation-sweep-all-registry-refs", + "tsrx-explicit-accessor-bindings", "tsrx-syntax-frontend", "two-weeks-smell", "typed-preload-links", "universal-renderer-effect-names", + "universal-spread-sources", "unstable-list-identity", "until-flip-entanglement", "until-live-source-ack", @@ -788,6 +876,7 @@ "update-signals-bugfix-baseline", "validate-malformed-html-error", "validate-prepare-request-return", + "view-table-on-enumeration", "violet-stingrays-end", "wait-asset-seam", "warm-doors-flow", @@ -800,6 +889,7 @@ "web-serialization-create-plugin", "web-server-dist-prod-mode", "why-did-this-run-attribution", + "write-proposals-3494", "written-keys-bound", "zombie-pair-extension-fast-drain" ] diff --git a/packages/babel-plugin/CHANGELOG.md b/packages/babel-plugin/CHANGELOG.md index ed1892028..cd8d02ba7 100644 --- a/packages/babel-plugin/CHANGELOG.md +++ b/packages/babel-plugin/CHANGELOG.md @@ -1,5 +1,24 @@ # @solidjs/babel-plugin +## 2.0.0-rc.9 + +### Patch Changes + +- 8d6de07: Native elements with several spread sources compile to the runtimes' array form instead of a `mergeProps()` call, in DOM and SSR output: `
` becomes `spread(el, [{ id: "x" }, a, b], …)` on the client and `ssrElement("div", [{ id: "x" }, a, b], …)` on the server. The runtimes read the sources directly — later sources win per key, only the winning source is read — with no merge proxy to build and walk, and a reactive spread is a plain thunk called inside the tracking scope, so it mints no memo and consumes no hydration id on either side (the hydratable SSR `() => mergeProps(…)` wrapper is gone for the same reason). A lone spread still passes straight through (#3105). Requires `@solidjs/web` with the `spread`/`ssrElement` array forms (#3418, #3419). +- 6d2bdeb: Move delegated event handlers off the `$$` element key Solid 1 uses. + + Solid 1 delegates from `document` and fires any `$$click`/`$$input`/… it finds while walking up from the target, so a 1.x runtime on the same page — an older embedded widget, a devtools panel built on 1.x — ran every delegated handler in a 2.x app a second time. Compiled output and the runtime now stamp `_$$` / `_$$Data` instead; neither version can see the other's handlers, in either nesting direction. + + The key, the `_$SOLID_EVENT_OWNER` mark, and the walk rules are documented in `client.ts` as the delegated-event wire contract shared by every Solid copy on a page. Anything reading `el.$$click` directly must switch to `el._$$click`. + +- 246eeeb: Emit non-identifier getter keys in compiled props literals as string literals (`get "aria-label"() {}`) instead of computed keys (`get ["aria-label"]() {}`). Same property, but a computed key drops the whole object literal off V8's boilerplate path into per-property runtime definition; on a seven-getter props literal the computed form costs ~45% more to build. Applies to every getter site in both compilers: component props, dynamic element attributes (DOM, SSR, universal). +- 63560a1: `componentNames` now applies to SSR output. Under the option both compilers keep the `createComponent` call they otherwise inline to `Comp(props)` and pass the source tag name — `createComponent(Comp, props, "Comp")` — so the server runtime's observe/dev `createComponent` labels the owner and a server finding's `ownerPath` reads `` like the client's. Without the option (prod builds) SSR output is unchanged. `@solidjs/vite-plugin` already passes the option for its dev and observe postures, so app server builds pick this up with no config change. + + Fixes `ssrScope` under transparent owners: the virtual hole scope swapped the current owner's id counter, but content inside a hole resolves ids by walking past transparent owners, so with one in between (the server-component scope owner; now the labelled component owner) the hole's content took ids from the enclosing counter and disagreed with the client. The scope now swaps the nearest id-bearing owner. + +- 350f65f: TSRX: `@for … index/key` items and `@catch` errors pass through to `For` / `Errored` as the accessors Solid hands out; the compilers no longer rewrite reads of those bindings into calls (#3474). Author `item()`, `i()` (under a custom key), and `err()` exactly as in JSX. A destructuring pattern in one of those positions is rejected with a diagnostic, since there is nothing to destructure — the default keyed `@for` item is still a raw value and still destructures. `projectTsrxForTypecheck` emits the same `(item, i) =>` / `(err, reset) =>` arrows `@tsrx/solid` does, so the two typecheck projections now agree. +- 1643d2a: The universal renderer's `spread()` follows the `@solidjs/web` contract (#3388): `ref` folds into the props effect and is re-applied only when its identity changes (refs run with no owner, so nothing they create is disposed by the fold); children keep their own owned `insert` — that effect owns the child subtree — but a plain object whose `children` is a data property inserts the value with no effect at all. Three reactive nodes become two when children flow through the spread, one when they don't. `spread` also resolves a lone function source inside its own tracking scopes and accepts an array of sources — `spread(node, [a, b], skipChildren)` — the union of their keys with later sources winning, only the winning source read, function sources called inline with no merge and no memo. Both compilers' universal output uses it: a lone spread passes straight through (reactive included, no more `mergeProps(() => …)`), and several sources compile to the array instead of a `mergeProps()` call. + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/babel-plugin/package.json b/packages/babel-plugin/package.json index ef17c5206..775382b76 100644 --- a/packages/babel-plugin/package.json +++ b/packages/babel-plugin/package.json @@ -1,7 +1,7 @@ { "name": "@solidjs/babel-plugin", "description": "Babel compiler plugin for Solid templates", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "author": "Ryan Carniato", "license": "MIT", "repository": { diff --git a/packages/compiler/CHANGELOG.md b/packages/compiler/CHANGELOG.md index 3091161bd..f1f6dc1d5 100644 --- a/packages/compiler/CHANGELOG.md +++ b/packages/compiler/CHANGELOG.md @@ -1,5 +1,24 @@ # @solidjs/compiler +## 2.0.0-rc.9 + +### Patch Changes + +- 8d6de07: Native elements with several spread sources compile to the runtimes' array form instead of a `mergeProps()` call, in DOM and SSR output: `
` becomes `spread(el, [{ id: "x" }, a, b], …)` on the client and `ssrElement("div", [{ id: "x" }, a, b], …)` on the server. The runtimes read the sources directly — later sources win per key, only the winning source is read — with no merge proxy to build and walk, and a reactive spread is a plain thunk called inside the tracking scope, so it mints no memo and consumes no hydration id on either side (the hydratable SSR `() => mergeProps(…)` wrapper is gone for the same reason). A lone spread still passes straight through (#3105). Requires `@solidjs/web` with the `spread`/`ssrElement` array forms (#3418, #3419). +- 6d2bdeb: Move delegated event handlers off the `$$` element key Solid 1 uses. + + Solid 1 delegates from `document` and fires any `$$click`/`$$input`/… it finds while walking up from the target, so a 1.x runtime on the same page — an older embedded widget, a devtools panel built on 1.x — ran every delegated handler in a 2.x app a second time. Compiled output and the runtime now stamp `_$$` / `_$$Data` instead; neither version can see the other's handlers, in either nesting direction. + + The key, the `_$SOLID_EVENT_OWNER` mark, and the walk rules are documented in `client.ts` as the delegated-event wire contract shared by every Solid copy on a page. Anything reading `el.$$click` directly must switch to `el._$$click`. + +- 246eeeb: Emit non-identifier getter keys in compiled props literals as string literals (`get "aria-label"() {}`) instead of computed keys (`get ["aria-label"]() {}`). Same property, but a computed key drops the whole object literal off V8's boilerplate path into per-property runtime definition; on a seven-getter props literal the computed form costs ~45% more to build. Applies to every getter site in both compilers: component props, dynamic element attributes (DOM, SSR, universal). +- 63560a1: `componentNames` now applies to SSR output. Under the option both compilers keep the `createComponent` call they otherwise inline to `Comp(props)` and pass the source tag name — `createComponent(Comp, props, "Comp")` — so the server runtime's observe/dev `createComponent` labels the owner and a server finding's `ownerPath` reads `` like the client's. Without the option (prod builds) SSR output is unchanged. `@solidjs/vite-plugin` already passes the option for its dev and observe postures, so app server builds pick this up with no config change. + + Fixes `ssrScope` under transparent owners: the virtual hole scope swapped the current owner's id counter, but content inside a hole resolves ids by walking past transparent owners, so with one in between (the server-component scope owner; now the labelled component owner) the hole's content took ids from the enclosing counter and disagreed with the client. The scope now swaps the nearest id-bearing owner. + +- 350f65f: TSRX: `@for … index/key` items and `@catch` errors pass through to `For` / `Errored` as the accessors Solid hands out; the compilers no longer rewrite reads of those bindings into calls (#3474). Author `item()`, `i()` (under a custom key), and `err()` exactly as in JSX. A destructuring pattern in one of those positions is rejected with a diagnostic, since there is nothing to destructure — the default keyed `@for` item is still a raw value and still destructures. `projectTsrxForTypecheck` emits the same `(item, i) =>` / `(err, reset) =>` arrows `@tsrx/solid` does, so the two typecheck projections now agree. +- 1643d2a: The universal renderer's `spread()` follows the `@solidjs/web` contract (#3388): `ref` folds into the props effect and is re-applied only when its identity changes (refs run with no owner, so nothing they create is disposed by the fold); children keep their own owned `insert` — that effect owns the child subtree — but a plain object whose `children` is a data property inserts the value with no effect at all. Three reactive nodes become two when children flow through the spread, one when they don't. `spread` also resolves a lone function source inside its own tracking scopes and accepts an array of sources — `spread(node, [a, b], skipChildren)` — the union of their keys with later sources winning, only the winning source read, function sources called inline with no merge and no memo. Both compilers' universal output uses it: a lone spread passes straight through (reactive included, no more `mergeProps(() => …)`), and several sources compile to the array instead of a `mergeProps()` call. + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/compiler/npm/darwin-arm64/package.json b/packages/compiler/npm/darwin-arm64/package.json index d427695d2..1c554263a 100644 --- a/packages/compiler/npm/darwin-arm64/package.json +++ b/packages/compiler/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@solidjs/compiler-darwin-arm64", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "cpu": [ "arm64" ], diff --git a/packages/compiler/npm/darwin-x64/package.json b/packages/compiler/npm/darwin-x64/package.json index 8e046f74f..7841eeb18 100644 --- a/packages/compiler/npm/darwin-x64/package.json +++ b/packages/compiler/npm/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@solidjs/compiler-darwin-x64", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "cpu": [ "x64" ], diff --git a/packages/compiler/npm/linux-arm64-gnu/package.json b/packages/compiler/npm/linux-arm64-gnu/package.json index 2b5207267..7a473938d 100644 --- a/packages/compiler/npm/linux-arm64-gnu/package.json +++ b/packages/compiler/npm/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@solidjs/compiler-linux-arm64-gnu", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "cpu": [ "arm64" ], diff --git a/packages/compiler/npm/linux-x64-gnu/package.json b/packages/compiler/npm/linux-x64-gnu/package.json index af8dd9847..d0affc884 100644 --- a/packages/compiler/npm/linux-x64-gnu/package.json +++ b/packages/compiler/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@solidjs/compiler-linux-x64-gnu", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "cpu": [ "x64" ], diff --git a/packages/compiler/npm/wasm32-wasi/package.json b/packages/compiler/npm/wasm32-wasi/package.json index 2749a8050..d34b29b99 100644 --- a/packages/compiler/npm/wasm32-wasi/package.json +++ b/packages/compiler/npm/wasm32-wasi/package.json @@ -1,6 +1,6 @@ { "name": "@solidjs/compiler-wasm32-wasi", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "main": "compiler.wasi.cjs", "files": [ "compiler.wasm32-wasi.wasm", diff --git a/packages/compiler/npm/win32-x64-msvc/package.json b/packages/compiler/npm/win32-x64-msvc/package.json index 270f4c33e..d38212f4a 100644 --- a/packages/compiler/npm/win32-x64-msvc/package.json +++ b/packages/compiler/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@solidjs/compiler-win32-x64-msvc", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "cpu": [ "x64" ], diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 6ffbd670d..f8a234a17 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,7 +1,7 @@ { "name": "@solidjs/compiler", "description": "Solid's native Oxc JSX compiler", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "author": "Ryan Carniato", "license": "MIT", "repository": { @@ -62,11 +62,11 @@ "emnapi": "^1.11.3" }, "optionalDependencies": { - "@solidjs/compiler-darwin-x64": "2.0.0-rc.8", - "@solidjs/compiler-darwin-arm64": "2.0.0-rc.8", - "@solidjs/compiler-linux-x64-gnu": "2.0.0-rc.8", - "@solidjs/compiler-linux-arm64-gnu": "2.0.0-rc.8", - "@solidjs/compiler-win32-x64-msvc": "2.0.0-rc.8", - "@solidjs/compiler-wasm32-wasi": "2.0.0-rc.8" + "@solidjs/compiler-darwin-x64": "2.0.0-rc.9", + "@solidjs/compiler-darwin-arm64": "2.0.0-rc.9", + "@solidjs/compiler-linux-x64-gnu": "2.0.0-rc.9", + "@solidjs/compiler-linux-arm64-gnu": "2.0.0-rc.9", + "@solidjs/compiler-win32-x64-msvc": "2.0.0-rc.9", + "@solidjs/compiler-wasm32-wasi": "2.0.0-rc.9" } } diff --git a/packages/diagnostics/CHANGELOG.md b/packages/diagnostics/CHANGELOG.md index 65de3e371..8b03ae7dd 100644 --- a/packages/diagnostics/CHANGELOG.md +++ b/packages/diagnostics/CHANGELOG.md @@ -1,5 +1,99 @@ # @solidjs/diagnostics +## 2.0.0-rc.9 + +### Patch Changes + +- eaa7e33: The attribution engine's folds, queries and formatters are named exports of `@solidjs/signals/attribution` (and `solid-js/attribution`), not methods of `attribution`. + - `costs()`, `feedback()`, `why(target)`, `subscriptions(target)`, `formatRerun(event)`, `formatOrigin(origin)` — import them by name. `attribution` keeps `enable`, `disable`, `subscribe`, `markFlight` and the record ring buffers (`history`, `holds`, `interactions`, `navigations`, `waterfalls`). `attribution.format` is `formatRerun`. + - Why: the fold tables (`costs`, `feedback`) are the dev/agent view of the records and the part of the engine that grows; a production adapter consumes records and never calls them. Each fold's module registers its accounting with the engine's fold seam when it is imported, so under the package's `sideEffects: false` a consumer that only subscribes to records ships neither the tables nor the work of filling them — importing `costs` or `feedback` is what turns them on. Measured: −1,150 B brotli for a records consumer (the size scenario's cap ratcheted to 27.25 KB); tree-shake tests pin it from source and from the built observe artifact. + - New types `AttributionCostTables`; `ScopeCost`/`WriteCost` and the feedback types move with their modules and are still exported from the entry. The prod tier's inert twin exports the same named surface, typed against the real one. + - `@solidjs/diagnostics`: `AttributionCosts`/`AttributionFeedback` are aliases of `AttributionCostTables`/`AttributionFeedbackTables`; the capture and the browser bridge import the folds by name. Artifact shape unchanged. + +- ebedb44: Attribution re-run records are serializable as emitted, and the observe tier's idle cost is a cap. + - `RerunEvent` no longer carries the live `node`. It names its scope by `nodeId` — the engine's per-node id, stable across the scope's runs in the process and distinct between scopes (so runs of unnamed effects still fold to one scope after the record has left the process). In-process consumers that want the node ask `OBSERVE.subjectOf(event)`, which now answers for re-run records as it did for diagnostic events, for as long as the caller holds the record object. `attribution.why(target)` and `subscriptions(target)` are unchanged. + - `@solidjs/diagnostics` artifact format v7: re-runs are stored verbatim (`RerunRecord` is now an alias of `RerunEvent`), and the artifact gains `timeOrigin` — the capturing process's `performance.timeOrigin` — so every relative `at` in it (re-runs, holds, records, diagnostic `data`) is convertible to absolute time after the fact, and a server capture lines up with the browser session it served. The JSONL meta line carries it too; the browser bridge payload includes it. + - New tripwire in the signals suite: the built observe artifact runs a graph-heavy workload within 1.25× of the built prod artifact with no hooks installed (measured 1.03–1.09). The idle wiring cost was informational before; it is capped now. + +- 3ae9e92: `OBSERVE.records` — one records channel on both platforms (observe/dev tiers); frame records from both ends; the client `"call"` record; `observeServerFunctionCalls` removed + - **`@solidjs/signals`**: `OBSERVE.records` — `subscribe(type, listener)`, `observed(type)`, `emit(type, event, live)` — the channel every runtime record rides, created once per process and registered on `globalThis` under `Symbol.for("@solidjs/signals/observe/records")` so a second copy of the core (a bundled server build instrumented through `--import`) and wire layers bundled without a framework import reach the same listener sets. Listeners are snapshotted per emit; a throwing listener is reported and the rest run. Types: `Records`, `RecordTypes` (extends `HostRecordTypes`; both declared empty, for the runtimes to augment — one augmenter per interface), `RecordType`, `RecordEvent`, `RecordLive`, `RecordListener`. Folds out of prod. New **`OBSERVE.attribution.currentOrigin()`** (and the `currentOrigin` hook on `AttributionHooks`): the provenance a root write performed now would be stamped with — the interaction whose handler is running, the navigation/effect/action frame open, or inside a recompute the origin of the change that caused it — as the engine's own `ChangeOrigin` object, `undefined` when external or with no engine; for a runtime stamping a record of its own. The installed hooks are also registered on `globalThis` under `Symbol.for("@solidjs/signals/observe/attribution")`, the same reach-without-an-import the channel has. + - **`solid-js`**: the `"boundary"` record moves from `OBSERVE.server.records` to `OBSERVE.records` (augmenting the core's `RecordTypes`). `OBSERVE.server` keeps only the `trace` slot; `ServerRecords` is gone. + - **`@solidjs/web`**: the `"invocation"` and `"frame"` records move to `OBSERVE.records` (augmenting `HostRecordTypes` through `solid-js`). New **`"call"` record** (`CallEvent`, `CallLive`, `CallListener`): one per server-function call made from the browser, at the caller's settle — `{ id, at, durationMs, method: "GET" | "POST", outcome, status?, origin?, deferred? }` with `{ args, response?, result? | error? }` beside it; joins the server's `"invocation"` by `id`, and — through `origin`, the engine's own interaction/navigation object read at dispatch via `currentOrigin()` — the attribution engine's `InteractionEvent` / `NavigationEvent` / `HoldEvent` by identity, so an observer files the call under the click that made it without a time join. The **`"frame"` record now has a client half**: `FrameEvent` is `FrameProducedEvent | FrameAppliedEvent`, discriminated by `side`, same census on both; the client half (`applyFrameResponse`, one per stream in a response) adds `address` (the `as` remap) and `outcome: "truncated"` for a body that ended before `complete`, with `live.response`. Server census fix: `regions` counts `html` chunks addressed to a child frame id (the former count read a chunk type that does not exist), and `shellMs` is set by the stream's own shell only. The emitters and their wrappers fold out of the prod client artifacts behind the observe literal (prod `applyFrameResponse` and the server-function dispatch are the pre-existing functions, no extra frame or promise hop). The server-functions and frames **client** entries gain `observe` and `development` builds and export conditions (`server-functions/dist/client.{observe,dev}.js`, `frames/dist/client.observe.js`); the server-functions client is now built with its flags replaced in every tier (before, `_SOLID_DEV_` there was an unreplaced truthy string). + - **Removed**: `observeServerFunctionCalls` and the `ServerFunctionCall` / `ServerFunctionRequestCall` / `ServerFunctionResponseCall` types, from both server-function entries. Subscribe to `OBSERVE.records` `"call"` (client) or `"invocation"` (server) instead. + - **`@solidjs/diagnostics`** (format v6): `artifact.server` is replaced by `artifact.records: { boundary, invocation, frame, call }` — always present, captured on both platforms including the browser bridge; types `BoundaryRecord`, `InvocationRecord`, `FrameRecord` (`FrameProducedRecord | FrameAppliedRecord`), `CallRecord` (with `origin?: ChangeOrigin`), `ArtifactRecords` replace the `Server*Record` / `ArtifactServer` names. JSONL: one line per record with `type` naming its table; the meta line's `boundaryCount`/`invocationCount`/`frameCount` become `recordCounts: { boundary, invocation, frame, call }`. + +- 0d8347a: Server records reach the diagnostics artifact and the dev checks (server-dev-build-plan P4) + - `@solidjs/diagnostics` artifact format **v5**: `artifact.server: { boundaries, invocations } | null` folds `OBSERVE.server.records` when the scenario runs under the server runtime — `captureArtifact(() => renderToStream(…))` — one row per `` boundary that waited and per server-function execution; `null` for client captures and the browser bridge. New exported types `ArtifactServer`, `ServerBoundaryRecord`, `ServerInvocationRecord` (mirrors of the runtime's `BoundaryEvent`/`InvocationEvent`; the package still depends on `@solidjs/signals` alone). JSONL egress adds `boundary` and `invocation` lines and the header counts. + - `InvocationEvent.boundary`: a direct server-function call made during a `` boundary's render pass carries that boundary's hydration id, the `"boundary"` record's `id` — the join between a boundary's wait and the calls under it. + - Two dev checks derived from the boundary facts in `ssrLoadingBoundary`: `ASYNC_WATERFALL` with `data.side: "server"` (`passes - 1` sequential flights; 2 → `info`, structured only; 3+ → console `warn`) and a new code `SSR_CLIENT_CONTENT_MASKED` (`warn`, `ssr`) for client-only content that surfaced only after a real server wait — the server's work discarded, the fallback shown for the wait. Dev tier only; the boundary clock now runs in dev without a listener. + - `solid-js`'s server `emitFinding` keeps `info` findings off the console (structured channel only), matching the core. + +- Updated dependencies [8ff4803] +- Updated dependencies [e9c464b] +- Updated dependencies [0da94f9] +- Updated dependencies [8bf04ea] +- Updated dependencies [eaa7e33] +- Updated dependencies [ebedb44] +- Updated dependencies [a8a8949] +- Updated dependencies [d80cd1f] +- Updated dependencies [d826cd3] +- Updated dependencies [cc0396b] +- Updated dependencies [d2a36f5] +- Updated dependencies [50323b4] +- Updated dependencies [53280e7] +- Updated dependencies [d7cb456] +- Updated dependencies [a0d6dd2] +- Updated dependencies [c3ae310] +- Updated dependencies [6095955] +- Updated dependencies [e80f241] +- Updated dependencies [84adf0b] +- Updated dependencies [5c1f01f] +- Updated dependencies [a5d8eae] +- Updated dependencies [14ded24] +- Updated dependencies [25c5064] +- Updated dependencies [1ce0f85] +- Updated dependencies [05c7e21] +- Updated dependencies [9da7f0a] +- Updated dependencies [62b0a22] +- Updated dependencies [27b24aa] +- Updated dependencies [549f482] +- Updated dependencies [d80cd1f] +- Updated dependencies [76230f9] +- Updated dependencies [347a5ca] +- Updated dependencies [a8a8949] +- Updated dependencies [632e45c] +- Updated dependencies [75c5113] +- Updated dependencies [899c2c4] +- Updated dependencies [ca05917] +- Updated dependencies [3ae9e92] +- Updated dependencies [328580f] +- Updated dependencies [0bffee2] +- Updated dependencies [f329a26] +- Updated dependencies [c827758] +- Updated dependencies [6e9243c] +- Updated dependencies [7a09cd9] +- Updated dependencies [61a114c] +- Updated dependencies [0d8347a] +- Updated dependencies [7623ce1] +- Updated dependencies [af94f67] +- Updated dependencies [e87d694] +- Updated dependencies [dd19e9e] +- Updated dependencies [c245532] +- Updated dependencies [34287d8] +- Updated dependencies [64f9266] +- Updated dependencies [0148d58] +- Updated dependencies [765a656] +- Updated dependencies [9db33cf] +- Updated dependencies [bfd6f6c] +- Updated dependencies [2054045] +- Updated dependencies [c410709] +- Updated dependencies [f555ec2] +- Updated dependencies [d8e35a3] +- Updated dependencies [5f7da9d] +- Updated dependencies [7f5f902] +- Updated dependencies [31adfce] + - @solidjs/signals@2.0.0-rc.9 + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/diagnostics/package.json b/packages/diagnostics/package.json index 5050b9b0d..1485f8035 100644 --- a/packages/diagnostics/package.json +++ b/packages/diagnostics/package.json @@ -1,6 +1,6 @@ { "name": "@solidjs/diagnostics", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "description": "Agent-consumable diagnostics harness for Solid: capture the dev-mode diagnostic and attribution channels as structured artifacts, with assertion helpers for tests and agent loops.", "author": "Ryan Carniato", "license": "MIT", @@ -52,7 +52,7 @@ "test:watch": "vitest watch tests" }, "dependencies": { - "@solidjs/signals": "^2.0.0-rc.8" + "@solidjs/signals": "^2.0.0-rc.9" }, "peerDependencies": { "vitest": ">=2.0.0" diff --git a/packages/element/CHANGELOG.md b/packages/element/CHANGELOG.md index 5a3bda285..7ad5383e0 100644 --- a/packages/element/CHANGELOG.md +++ b/packages/element/CHANGELOG.md @@ -1,5 +1,50 @@ # solid-element +## 2.0.0-rc.9 + +### Patch Changes + +- Updated dependencies [8bf04ea] +- Updated dependencies [d826cd3] +- Updated dependencies [cc0396b] +- Updated dependencies [6d2bdeb] +- Updated dependencies [17b0bda] +- Updated dependencies [1af28a1] +- Updated dependencies [d2a36f5] +- Updated dependencies [c452850] +- Updated dependencies [53280e7] +- Updated dependencies [5b31076] +- Updated dependencies [084e621] +- Updated dependencies [a429b44] +- Updated dependencies [549f482] +- Updated dependencies [3154ed6] +- Updated dependencies [b298154] +- Updated dependencies [899c2c4] +- Updated dependencies [3ae9e92] +- Updated dependencies [328580f] +- Updated dependencies [da6ed76] +- Updated dependencies [5f688a6] +- Updated dependencies [c66130d] +- Updated dependencies [36db287] +- Updated dependencies [61a114c] +- Updated dependencies [0d8347a] +- Updated dependencies [7623ce1] +- Updated dependencies [56858e7] +- Updated dependencies [537faea] +- Updated dependencies [af94f67] +- Updated dependencies [042b540] +- Updated dependencies [e87d694] +- Updated dependencies [7f6332a] +- Updated dependencies [5426ffb] +- Updated dependencies [63560a1] +- Updated dependencies [a732d2b] +- Updated dependencies [40977c9] +- Updated dependencies [34287d8] +- Updated dependencies [48f007e] +- Updated dependencies [7f5f902] + - solid-js@2.0.0-rc.9 + - @solidjs/web@2.0.0-rc.9 + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/element/package.json b/packages/element/package.json index f0f953103..b0efb16e2 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -3,7 +3,7 @@ "description": "Webcomponents wrapper for Solid", "author": "Ryan Carniato", "license": "MIT", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "homepage": "https://github.com/solidjs/solid/blob/main/packages/element#readme", "repository": { "type": "git", @@ -30,8 +30,8 @@ "component-register": "^0.8.7" }, "peerDependencies": { - "solid-js": "^2.0.0-rc.8", - "@solidjs/web": "^2.0.0-rc.8" + "solid-js": "^2.0.0-rc.9", + "@solidjs/web": "^2.0.0-rc.9" }, "devDependencies": { "solid-js": "workspace:*", diff --git a/packages/h/CHANGELOG.md b/packages/h/CHANGELOG.md index ee3f70689..c31113f4b 100644 --- a/packages/h/CHANGELOG.md +++ b/packages/h/CHANGELOG.md @@ -1,5 +1,50 @@ # @solidjs/h +## 2.0.0-rc.9 + +### Patch Changes + +- c452850: `dynamic()` / `Dynamic` with a tag-name source honor an `xmlns` prop when creating the element (#3386) + + The compiler resolves a tag's namespace from its parent at build time; the `dynamic()` runtime path creates the element before it has a parent, so a tag that exists in both HTML and SVG (`a`, `script`, `style`, `title`) was always created as an HTML element — `` with `Link = dynamic(() => "a")` produced an HTML anchor inside the SVG tree. The instance can now say which one it means with the same attribute compiled JSX uses for the same purpose: ``. Like `is`, `xmlns` is read once, untracked, at creation (the DOM can't re-namespace a node) and then applied as an ordinary attribute, so a client-rendered element carries the same attribute the server serializes. Without `xmlns` the namespace is still inferred from the tag name. Hydration is unaffected: it claims the parser-namespaced node. + + Types: `xmlns` is now accepted on the four tags that exist in both HTML and SVG (`a`, `script`, `style`, `title`) in addition to the SVG/MathML attribute sets that already had it — those are the only tags where the attribute changes what gets created, and the compiler has honored `` on them all along. Unambiguous HTML tags (`div`, `span`, …) still reject it. `dynamic()`'s tag-name components inherit it through their intrinsic attribute types. Also syncs `@solidjs/h`'s generated JSX types. + +- Updated dependencies [d826cd3] +- Updated dependencies [6d2bdeb] +- Updated dependencies [17b0bda] +- Updated dependencies [1af28a1] +- Updated dependencies [d2a36f5] +- Updated dependencies [c452850] +- Updated dependencies [53280e7] +- Updated dependencies [5b31076] +- Updated dependencies [084e621] +- Updated dependencies [a429b44] +- Updated dependencies [549f482] +- Updated dependencies [3154ed6] +- Updated dependencies [b298154] +- Updated dependencies [899c2c4] +- Updated dependencies [3ae9e92] +- Updated dependencies [328580f] +- Updated dependencies [da6ed76] +- Updated dependencies [5f688a6] +- Updated dependencies [c66130d] +- Updated dependencies [36db287] +- Updated dependencies [61a114c] +- Updated dependencies [0d8347a] +- Updated dependencies [7623ce1] +- Updated dependencies [56858e7] +- Updated dependencies [537faea] +- Updated dependencies [af94f67] +- Updated dependencies [042b540] +- Updated dependencies [7f6332a] +- Updated dependencies [a732d2b] +- Updated dependencies [40977c9] +- Updated dependencies [34287d8] +- Updated dependencies [48f007e] +- Updated dependencies [7f5f902] + - @solidjs/web@2.0.0-rc.9 + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/h/package.json b/packages/h/package.json index 458c86edd..0771aca02 100644 --- a/packages/h/package.json +++ b/packages/h/package.json @@ -1,7 +1,7 @@ { "name": "@solidjs/h", "description": "Hyperscript / h() factory for Solid — write components without compiled JSX.", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "author": "Ryan Carniato", "license": "MIT", "homepage": "https://solidjs.com", @@ -58,7 +58,7 @@ "test": "vitest run" }, "peerDependencies": { - "@solidjs/web": "^2.0.0-rc.8" + "@solidjs/web": "^2.0.0-rc.9" }, "devDependencies": { "@solidjs/web": "workspace:*", diff --git a/packages/html/CHANGELOG.md b/packages/html/CHANGELOG.md index 60d698416..839dd2e24 100644 --- a/packages/html/CHANGELOG.md +++ b/packages/html/CHANGELOG.md @@ -1,5 +1,65 @@ # @solidjs/html +## 2.0.0-rc.9 + +### Patch Changes + +- 899c2c4: `merge()` and `omit()` are always lazy views, and props consumers read their leaves + + `omit(props, ...keys)` returns a live view of `props` for every input — a plain object included — instead of copying it with a `getOwnPropertyDescriptor` + `defineProperty` per prop. A predicate form hides keys by rule without enumerating first: `omit(props, k => k[0] === "$")`. `merge()` no longer builds an eager copy when its sources are plain objects: under `Proxy` it always returns an O(1) view over the flattened sources (a single non-function source is returned as is). + + The two compose flat. An `omit()` over a `merge()` carries one filtered view per flattened merge source, a `merge()` over an `omit()` takes the view record as a leaf, and nested omits fold their filters into one record. A component chain of `merge(defaults) → omit(consumed) → merge(statics) → omit("as")` — the shape headless-UI libraries render every element through — collapses to leaf views over the original objects, each with its accumulated filter, with no proxy layer left between the outermost spread and the author's props. `merge()` keeps the omitted keys hidden by construction (#3014) rather than by treating the omit as opaque. Construction cost drops 3–7× at depth 1–7; the SSR polymorphic-chain bench (#3448) runs ~2.4× faster. + + Reads stay cheap: a view over plain objects resolves a key → owning-leaf table once, on first read, and every `get`/`has`/descriptor is one lookup after that. `spread()` (DOM and universal) and `ssrElement()` read the leaves directly — never through the proxies' traps — and walk that table when there is one, so an effect rerun costs one read per key, as it did over the copy. Both proxies use a class target and one shared handler (no per-instance closures). + + A view over a store asks the store nothing but the read. Each source's kind (plain object, omit record, proxy, memo) is decided once, when the view is built, and carried beside it — every brand check on a Proxy is a trap (`instanceof` is a `getPrototypeOf` trap, as expensive as a store read), and store detection goes through `$TARGET`, a symbol the store's `get` trap answers on its fast path, never its generic tracked-read path. `merge(defaults, store)` constructs ~30% faster than the copy did and reads ~15% faster; `omit(store)` reads at parity. + + The views tell the truth: `Object.getOwnPropertyDescriptor(view, key)` reports a data descriptor only when the key is a data property of a plain leaf (the compiler's encoding of a static prop) and an accessor for a getter, a store key, or a memo source. Together with the new internal `hasStaticKeys()`, `spread()` now skips the children effect for static children behind `omit`/`merge` layers (#3388 through views). + + Behavior changes: + - Writes to a `merge()` or `omit()` result are no-ops (they already were for the proxy forms). A caller that needs its own object copies it (`{ ...merged }`), and the copy carries no sources (#3384). `@solidjs/html` now collects its own props and spreads into one `merge()` at the end instead of assigning onto the result. + - A data property on a source is read live through the view rather than snapshotted at `merge()`/`omit()` time. + - Key order of a merged view is the merged order — every key at the position of the last source that carries it — matching `ssrElement`'s array form. + - Sources are treated as own-keyed; a key added to a plain source after merging is not seen (the copy did not see it either). + - Enumerating a view through its traps (`for…in`, `Object.keys`, `{ ...view }`) costs a trap per key, as any proxy does; the internal consumers avoid it. Environments without `Proxy` keep the copy paths. + + Internal helpers for consumers, exported from `solid-js`: `viewOf(o)`, `mergeView(o)`, `omitView(o)`, `sourceKeys(entry, kind)`, `sourceHas(entry, kind, key)`, `sourceGet(entry, kind, key)`, `hasStaticKeys(o)`, `resolvedTable(o)`, the `SOURCE_*` kinds. + +- Updated dependencies [d826cd3] +- Updated dependencies [6d2bdeb] +- Updated dependencies [17b0bda] +- Updated dependencies [1af28a1] +- Updated dependencies [d2a36f5] +- Updated dependencies [c452850] +- Updated dependencies [53280e7] +- Updated dependencies [5b31076] +- Updated dependencies [084e621] +- Updated dependencies [a429b44] +- Updated dependencies [549f482] +- Updated dependencies [3154ed6] +- Updated dependencies [b298154] +- Updated dependencies [899c2c4] +- Updated dependencies [3ae9e92] +- Updated dependencies [328580f] +- Updated dependencies [da6ed76] +- Updated dependencies [5f688a6] +- Updated dependencies [c66130d] +- Updated dependencies [36db287] +- Updated dependencies [61a114c] +- Updated dependencies [0d8347a] +- Updated dependencies [7623ce1] +- Updated dependencies [56858e7] +- Updated dependencies [537faea] +- Updated dependencies [af94f67] +- Updated dependencies [042b540] +- Updated dependencies [7f6332a] +- Updated dependencies [a732d2b] +- Updated dependencies [40977c9] +- Updated dependencies [34287d8] +- Updated dependencies [48f007e] +- Updated dependencies [7f5f902] + - @solidjs/web@2.0.0-rc.9 + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/html/package.json b/packages/html/package.json index 1e2e5f253..74d5d8f51 100644 --- a/packages/html/package.json +++ b/packages/html/package.json @@ -1,7 +1,7 @@ { "name": "@solidjs/html", "description": "Tagged-template-literal templating for Solid — write components with no build step.", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "author": "Ryan Carniato", "license": "MIT", "homepage": "https://solidjs.com", @@ -42,7 +42,7 @@ "test": "vitest run" }, "peerDependencies": { - "@solidjs/web": "^2.0.0-rc.8" + "@solidjs/web": "^2.0.0-rc.9" }, "devDependencies": { "@solidjs/web": "workspace:*", diff --git a/packages/signals/CHANGELOG.md b/packages/signals/CHANGELOG.md index 133ca6c67..13dcd793e 100644 --- a/packages/signals/CHANGELOG.md +++ b/packages/signals/CHANGELOG.md @@ -1,5 +1,257 @@ # @solidjs/signals +## 2.0.0-rc.9 + +### Patch Changes + +- 8ff4803: A write becomes visible at flush — to every channel (A28). Between `set(x)` and the flush that carries it, the write is not the committed value, not the staged value `latest()` / `isPending()` serve, and not an input to any derivation created meanwhile: `latest(x)` answers the pre-write value, `isPending(x)` is false, `until()`'s predicate evaluated in the carrying flush sees it. Optimistic writes are writes too (A28 (5), "match React"): `setOptimistic(v)` becomes the active override at the flush that carries it — plain reads, `snapshot()`, `in`, keys, `length` and `isPending()` see nothing before — while the writer's own channels (a functional updater, the store draft, the `affects()` declaration walk) compose on it. A rewrite of a node a transaction holds keeps the staged value the last flush left for `latest()`/verdicts until the next flush. Companions and store keys first materialized under a hold are born as the holding transaction's (#3336). + + Landed as a read-side rule rather than #3337's deferred subscriber walk: "unflushed" is structural (an ambient staged value outside a flush), the plain write path is untouched, and readers served the flushed value are latched for the carrying flush. Supersedes the #2922 mid-tick `latest()` pull (`flush()` first to read your own write) and re-pins the pre-A28 expectations accordingly. + +- e9c464b: Docs: inside an `action`, a bare `yield` is required after an `await` before anything that creates a reader — `until()`, `latest()`, a memo or effect, a mount — not only before writes. The `until()` docstring's own example had `await` straight into `yield until(...)`; the `until(...)` expression is evaluated in the post-`await` continuation, outside the transaction, and its predicate reader is born held there (#3482). Example corrected. +- 0da94f9: An async memo's held landing keeps the committed frame's dependencies (#3461). + - `selected = createMemo(async () => (b() ? b() : a()))` with `b` held by a slow flight: selected's held pass read only `b`, and its landing trimmed `a` at once, before the write staged the landed value under the hold. A later mainline `a` write no longer reached selected, so `A: 1` committed beside `Selected: 0` while `B` still read 0. The landing now trims only when it published (A30, the landing twin of a staged sync pass); a transition-held landing leaves the tail for its commit, so the `a` write reaches selected and joins the hold, and the four values reveal as one frame. + +- 8bf04ea: New dev diagnostic `ASYNC_STORE_SETTER`: a store setter callback that returns a Promise now throws in dev (through the diagnostics channel) instead of being silently ignored. A store setter is a synchronous transaction — the draft closes when the callback returns — so `setStore(async d => …)` committed only the writes before its first `await` and lost the rest. Covers every store family with a user setter (`createStore`, `createOptimisticStore`, the derived store's manual setter); a derived store's own async compute is not affected. Store-specific: a signal may legitimately hold a promise, so `setSignal` has no such rule. Production is unchanged. + + Docs: the `OBSERVE.exclude` guidance in RFC 08 no longer suggests `runWithOwner(panelRoot, …)` for panel writes (that is a write in an owned scope, see #3512); the store's own nodes carry the excluded owner. + +- eaa7e33: The attribution engine's folds, queries and formatters are named exports of `@solidjs/signals/attribution` (and `solid-js/attribution`), not methods of `attribution`. + - `costs()`, `feedback()`, `why(target)`, `subscriptions(target)`, `formatRerun(event)`, `formatOrigin(origin)` — import them by name. `attribution` keeps `enable`, `disable`, `subscribe`, `markFlight` and the record ring buffers (`history`, `holds`, `interactions`, `navigations`, `waterfalls`). `attribution.format` is `formatRerun`. + - Why: the fold tables (`costs`, `feedback`) are the dev/agent view of the records and the part of the engine that grows; a production adapter consumes records and never calls them. Each fold's module registers its accounting with the engine's fold seam when it is imported, so under the package's `sideEffects: false` a consumer that only subscribes to records ships neither the tables nor the work of filling them — importing `costs` or `feedback` is what turns them on. Measured: −1,150 B brotli for a records consumer (the size scenario's cap ratcheted to 27.25 KB); tree-shake tests pin it from source and from the built observe artifact. + - New types `AttributionCostTables`; `ScopeCost`/`WriteCost` and the feedback types move with their modules and are still exported from the entry. The prod tier's inert twin exports the same named surface, typed against the real one. + - `@solidjs/diagnostics`: `AttributionCosts`/`AttributionFeedback` are aliases of `AttributionCostTables`/`AttributionFeedbackTables`; the capture and the browser bridge import the folds by name. Artifact shape unchanged. + +- ebedb44: Attribution re-run records are serializable as emitted, and the observe tier's idle cost is a cap. + - `RerunEvent` no longer carries the live `node`. It names its scope by `nodeId` — the engine's per-node id, stable across the scope's runs in the process and distinct between scopes (so runs of unnamed effects still fold to one scope after the record has left the process). In-process consumers that want the node ask `OBSERVE.subjectOf(event)`, which now answers for re-run records as it did for diagnostic events, for as long as the caller holds the record object. `attribution.why(target)` and `subscriptions(target)` are unchanged. + - `@solidjs/diagnostics` artifact format v7: re-runs are stored verbatim (`RerunRecord` is now an alias of `RerunEvent`), and the artifact gains `timeOrigin` — the capturing process's `performance.timeOrigin` — so every relative `at` in it (re-runs, holds, records, diagnostic `data`) is convertible to absolute time after the fact, and a server capture lines up with the browser session it served. The JSONL meta line carries it too; the browser bridge payload includes it. + - New tripwire in the signals suite: the built observe artifact runs a graph-heavy workload within 1.25× of the built prod artifact with no hooks installed (measured 1.03–1.09). The idle wiring cost was informational before; it is capped now. + +- a8a8949: The window after an action body ends and its override is superseded by the committed truth (#3427) now reads like a landing supersession for every reader: a stale reader re-run by an unrelated write keeps displaying the override, a memo created mainline during the window is held with the transaction, and `isPending()` reads true while the truth differs from the override (`latest()` already answered the truth). The node carries no transaction stamp in that window — an override written inside an action never passes the adoption loop that stamps one — so `supersededRead` and the verdict now resolve the owning transaction through `_overrideOwner`. Also: a `latest()` / `isPending()` pull from mainline never enters a transaction (it is an observation); one did through the supersession path and captured the caller's synchronous block. +- d80cd1f: A memo or effect created from mainline code while a transaction holds a value it reads is now "born held" (A29, creation-time form): its creation pass derives from the transaction's staged world and is staged into that transaction — committed with it, and for an effect first run by its commit — instead of committing the held value into the mainline frame beside readers that show the committed one. The mainline block that created it is untouched: `enterStagedRead` no longer enters the transaction ambiently from creation code, so an unrelated write made after such a mount (a click that opens a panel while an action is in flight, then writes something else) is a mainline write again rather than being swallowed into the action. An untracked read of a born-held memo throws `NotReadyError` until the commit — it has no committed value to serve. +- d826cd3: The client error hook — `configureClientErrors({ onError })` from `solid-js`, and `render`/`hydrate`'s `onError` option in `@solidjs/web`: the prod-tier seam through which an app, or an error monitor, hears the one failure nothing else can see — an `` / `createErrorBoundary` collected it and renders its fallback. The twin of the server's `configureServerErrors`. An uncaught error is not this hook's: the halt (`REACTIVITY_HALTED`) hands its cause to the platform's `reportError`, the channel every monitor already listens on. + + Once per error object (a `reset()` re-collecting the same failure says nothing new; a primitive is reported per sight); `ownerPath` carries the component labels where the runtime keeps owner names; no return — the client has no wire to map for; a throwing hook is reported and ignored. A root's own hook wins over the ambient one for failures under it. + + Pay-for-use: the hook machinery (`core/error-hooks.ts`) is retained by `createErrorBoundary` or the app's own `configureClientErrors` import; a root's hook is parked on the root owner under the registered `ROOT_ERROR_HOOK` symbol (defined in the scheduler), so `render` retains nothing for an app that passes none. Core floor unchanged; apps with a boundary +~200 B. + + **The server surface consolidates on the same name.** `onError` on `renderToStream`/`renderToString` and on `handleServerFunctionRequest` _is_ the server error hook (`(error, context) => wire | void`); `onServerError` is removed. A one-argument `onError` written for the old shape keeps working and now hears every handled failure — filter on `context.handling === "failed"` for the request-failing ones alone. New `handling: "serialize"` for a hydration value that would not serialize (what seroval's `onError` reported, for a render that passed one). With no hook anywhere, a failure that fails the request still reaches `console.error`. + +- cc0396b: `_parent` joins `_name` as a field signals' property mangling reserves — the two cross-package owner fields. + + Signals' prod and observe artifacts rename every `_`-prefixed property except a reserved list; the dev artifact (which the test suites run against) is unmangled. Two things read `_parent` across the package boundary and only worked in dev: + - `solid-js`'s client hydration walks `owner._parent` to the root to mark the hydration snapshot scope. In the built prod and observe artifacts the walk found nothing and marked the current owner instead, so computations created outside that owner's subtree during hydration read live values rather than the server snapshot. + - The core's owner walks — `ownerPath` and `OBSERVE.exclude`/`isExcluded` — over `solid-js`'s server owners. `ownerPath` had a server-side shim (`located()`, now removed); `OBSERVE.exclude` was a silent no-op for a server owner outside dev. + + Cost: ~40 B brotli on the prod app scenarios; the observe scenarios did not grow. Pinned from both ends: `packages/solid/test/cross-package-fields.spec.ts` checks the reserved fields survive in the mangled artifacts and scans the built client artifacts of `solid-js`, `@solidjs/web` and `@solidjs/universal` for any signals `_` field that is not reserved; `packages/web/test/server/server-owner-walks.spec.tsx` runs `ownerPath` and `OBSERVE.exclude` over server owners against the built observe and development artifacts. + + Also: RFC 08 gains "Values in records — the PII surface", the complete list of record and finding fields that carry user data (value previews, interaction target text, navigation paths/params, `data.error` on the server error findings) for exporters that leave the process. + +- d2a36f5: `dynamic(source, { static })` and `isStatic(o, key)` + + `dynamic()` pays for a factory memo plus a per-instance memo so the source can + change. A great many call sites never change: a runtime `styled()` that always + renders `"li"`, and — the case this exists for — a polymorphic component whose + `as` arrived as a literal. The compiler encodes `as="button"` at a call site as + a data property and `as={isLink() ? "a" : "button"}` as a getter, so which one + the caller wrote is readable at runtime. + + `isStatic(o, key)` reads it: one descriptor lookup, no read of the value, + nothing tracked, looking through `merge()`/`omit()` views to the leaf that owns + the key. Data property or absent-from-a-fixed-key-set is static; a getter, a + store key, or a memo-backed `merge()` source is not. + + `dynamic(source, { static: true })` then says the source cannot change: it is + called once, untracked, at `dynamic()` time, and each instance renders the + result with no computation of its own — a tag goes to the compiled element path + (create or claim, spread), a component is called directly. No owner is created + on either side, so hydration ids stay aligned between server and client. A + static source may not return a promise. + + ```tsx + function Polymorphic(props) { + const Tag = dynamic(() => props.as, { static: isStatic(props, "as") }); + return ; + } + ``` + + `as` stays public and reactive; the literal case stops paying for it. Note the + two paths produce DIFFERENT hydration ids (the memo path's element sits one + owner deeper), which is fine because `isStatic` reads the same descriptors on + both sides — but it is why the classification must be per instance rather than + per component. + +- 50323b4: Keep a mainline-computed effect value out of a parked transaction. When an effect stamped by a held transaction recomputed on an unrelated write and no longer read the held source, the forced re-run inside that transaction re-claimed ownership of the value it had just published. A finalize-time re-entry (a `Loading` boundary's `on` reset flipping its fallback state) then parked the effect with the transaction, leaving a `show() ? details() : "hidden"` reader stale until the unrelated async settled (#3412). +- 53280e7: The error hooks and `SSR_RENDER_ERROR_CONTAINED` tell where an error was thrown apart from where it was met. + + `ownerPath` on `ClientErrorContext` and `ServerErrorContext` is now where the error was **thrown**: the labels root-first up the owner chain of the computation that threw — the component that broke — falling back to the boundary's chain when the throw crossed nothing the runtime could name. A new `boundaryPath` is where it was **met**: the same labels up the chain of the `` that rendered its fallback (client and server) or the `` that shipped the rejection (server, `handling: "client"`). Before, `ownerPath` was the boundary's on both sides, so every component under one boundary grouped into one path. On the client the engine's status wrapper already named the thrower (`StatusError.source`); on the server the owner scopes stamp it as the error escapes. The `SSR_RENDER_ERROR_CONTAINED` finding follows: `ownerPath` locates the throw, `data.boundary` / `data.boundaryPath` the boundary. + +- d7cb456: Fix a second write arriving while an async chain is still in flight (#3373, #3374, #3375, #3376). + - A flight's landing now retires only the node's own pending entry. When an input was re-asked mid-flight (`a` restarted while `b`'s first flight was up), `b` stays pending on `a`; the stale landing no longer let the transaction commit the newer signal beside the older derived value (`2 / 1`, #3373) or blip `isPending` to `false` (#3376). A fresh flight drops pending entries its inputs propagated earlier — the run read them, so a masked input (an active override, A17) does not hold it. + - A transaction now tests whether a source's own flight is still up by its self entry rather than `_error.source`, which a later-pending input overwrites; the held write no longer commits ahead of its answer once the load is re-asked under an `on`-scoped boundary (#3375). + - A collecting `Loading` boundary records every source the notifying effect is pending on, not only the one the notification carries — an `on` reset no longer reveals content when the boundary's one collected source settles while the effect is still pending on a flight it already carried (#3375). + - A render effect served a pending node's committed value (the A15 reveal carve-out) joins the transaction's reporters for that node, so a keyed remount that disposes the original reader no longer lets a same-value rewrite commit the held write while the derivation is in flight (`Count: 1` beside `Details: 0`, #3374). + - A `Loading` boundary's `on` reset ends the hold on writes that only its readers observed: a reader registered while the boundary showed content stops blocking once the boundary flips to its fallback, and the parked transaction is woken and re-judged in the same drain. A reader outside the boundary that also observes the flight still holds it (#3375). + +- a0d6dd2: Conditional JSX across a held branch change stays coherent with its inputs (#3438). + + An effect's dependencies are the committed frame's until its run applies — the effect twin of the memo rule from #3410. A pass that direct-committed its value but whose run was stashed with a hold (the same flush pended an async memo) used to trim the dependencies its previous run still displayed, so a later mainline write to one of them never reached the effect: `{show() ? count() : "hidden"}` held on `show → false` showed `Count: 1` beside `Panel: 0` while `Show` still read `true`. The trim now waits for the run to apply, so the write re-derives the effect against the committed inputs (`Panel: 1` beside `Count: 1`), and the hold's landing reveals `hidden` with `Show: false`. + +- c3ae310: Conditional memos across a held branch change stay coherent with their inputs (#3408, #3410). + - A tracked computation served a live transaction's staged value now enters that transaction, so its result is held with it (#3408). A memo whose branch flipped mainline and started reading a held signal previously published the staged value beside the signal's committed one (`Panel: 1` next to `Count: 0`). The read entry is the twin of the existing write-side (`setSignal`) and stamped-recompute entries. + - A memo's dependencies are the committed frame's until the frame is replaced (#3410): a pass that stages its value leaves the previous pass's dependency tail linked, and the commit trims it. A write to a dependency the committed value still derives from reaches the memo and joins its hold — as an unconditional read would — instead of revealing the write beside a stale committed derivation (`Count: 1` next to `Selected: 0` while `Fixed: false`). + +- 6095955: Two ways a held write stayed staged past the point it should have revealed: + - A transaction whose only reporter is disposed by ambient work (a `` unmounting the reader of a pending memo) was never re-judged — the flush only evaluates the active transaction, and nothing re-entered a parked one. The writes held with that reader (a signal set while it was pending) stayed staged forever. Disposing a pending reader parked in a transaction now wakes it; the flush re-enters a woken transaction on an otherwise idle pass, prunes the dead reporter and commits (#3372). + - An effect that reads `latest()` (or otherwise adopts an optimistic lane through its deps) direct-commits on a lane pass, but a hold it had staged on an earlier, lane-free pass of the same transaction was left in place and the transaction's commit published that older frame over the fresh value — `Pair: 0 / 0` for good. A lane recompute now drops the hold it supersedes, override or not (#3377). + +- e80f241: A node recomputed under a held transaction no longer tears down the committed frame's children immediately. Status propagation stamps a parked dependent with the transaction without recomputing it, so its owned children (nested render effects, memos, `onCleanup` registrations) still belong to what is on screen; when the pending source landed, the recompute disposed them on the spot and their cleanups ran mid-hold, before the transaction's atomic reveal (#3404). Those children are now deferred as zombies until the node commits, matching the plain-flush path. Children built by a recompute that never committed (a staged value, a pending window, a run under the transaction) are still disposed immediately on the next re-run — no frame ever showed them. +- 84adf0b: Fix a Loading boundary whose `on` key consults `isPending()` through a memo freezing the page (#3528). The key is evaluated from `notify`, inside the pending memo's own pass; reading the pending `isPending` memo there recorded an untracked-pending re-run dependency on that memo, making the async memo depend on a memo that depends on it — every pending mark re-derived it and it never converged. The `on` accessor is now evaluated as a spectator (`spectate`): untracked, and never recorded as a dependency of whichever node's pass it happens to run in. +- 5c1f01f: A `Loading` boundary whose `on` resets keeps its fallback until every reader under it has settled, not only the ones that notified after the reset (#3459). + - The reset clears the boundary's collected sources and re-collects from the pending notifications that follow. A reader already pending from an earlier write never re-notifies (status propagation dedupes on its `_pendingSources`), so a sibling reader's fresh flight was the only source collected, and its landing revealed the still-flying one stale: `B: 1 | Fast: 1 | Slow: 0` for two seconds. The reset now also harvests what its forwarded readers still wait on from the live transactions' `_asyncReporters` (INV-3, the one record of a forwarded reader), so the hold the #3375 ruling takes off the lane lands on the boundary instead: `B: 1 | Loading`, then `Fast: 1 | Slow: 1` together. + +- a5d8eae: A memo computes under its own lane posture, never its puller's (#3442). + + A combined `isPending(() => [fast(), copy()])` over two async memos, with `copy` a sync memo wrapping the slow one, released the hold as soon as the fast flight landed: `Fast: 1` beside `Slow: 0` with `Pending: false`, then `Slow: 1` a second later. The probe effect carries the companion lane of the pending signals it reads, and its pull of `copy` ran under that lane — where a pending node on no lane serves its committed value instead of throwing — so `copy` published a stale settled value, dropped its pending status, and its readers stopped holding the slow flight. `recompute` now runs a memo plain unless the memo itself is lane-dirty or adopts a lane through its dependencies; effects keep the ambient lane, since their runs are the lane's own view. Both values now reveal together, with the probe reporting pending until they do. + +- 14ded24: `merge()` no longer flattens through a plain-object merge result. That form is a real object callers may copy (`Reflect.ownKeys` descriptor copies, `{...props}`) or mutate afterwards (`@solidjs/html` assigns props and a `children` getter after spreading), and a later `merge` tunnelled back to the original sources through the `$SOURCES` symbol — resurrecting removed keys and dropping added or overwritten ones (#3384). The plain result now records no sources and is read like any other object; only merge _proxies_, whose writes are no-ops, are still flattened. A side effect: `merge(defaults, props)` where `props` is itself a plain merge result that covers every default now returns `props` directly instead of always allocating. +- 25c5064: Optimistic frames release when nothing authoritative is left to wait on, and a shared render effect no longer entangles unrelated updates (#3426, #3427, #3407). + - The last async reader of an optimistic value unmounting mid-action releases the frame (#3426): the lane's hold check prunes dead reporters itself (`sourceObserved`, shared with the settle verdict), instead of waiting for a flight nobody observes to land. + - The action body ending starts the correction (#3427): with the bodies over and no authoritative work in flight — no override node's own source, no held plain load the action asked for — each override's truth supersedes it now and the graph re-derives from it as the transaction's held work, settling when that lands. Before, the settle waited for the obsolete lane-derived flight, flashed the obsolete optimistic frame, then reverted and re-asked. A co-written flag stays through a plain load; optimistic store edits keep the settle-then-revert order. + - A render effect's pass belongs to whatever dirtied it (#3407): a sync `action` write to a signal that shared a hole with a held async (`{b()}:{detailsA()}`) merged into the async's transaction and waited (`0:0 → 2:1`) while the same plain write passed through. `recompute` re-enters a stamped node's transaction for memos only; the landing of a flight enters every transaction waiting on it (`enterWaiting`), which is where the effect re-entry's one legitimate job — completing the reveals that discovered the flight — now lives. Two independent flights read in one hole land at their own times. + +- 1ce0f85: Optimistic settle verdicts (#3409, #3411). + - `isPending(() => [a(), b()])` over two async siblings of an optimistic value reports pending as soon as either read is (#3409). `assignOrMergeLane` now follows a merged lane to its root and runs the parent/child check on it; the old "merged lane is stale, take the source lane" shortcut moved the combined probe's effect onto the held parent lane, where its verdict waited on the async it reports. + - An unowned `onSettled` callback (event handler, action body) reads a settled world (#3411). The settle only enqueues the reverted subscribers for the next pass, so a callback fired in the commit pass saw the optimistic source reverted beside a sync memo of it still holding the optimistic value. The fire now waits for the heap to drain. + +- 05c7e21: Two flights through one memo settle as one unit, and a branch a held `Show` is removing keeps following `latest()` (#3443, #3444). + - A memo another live transaction holds, made pending by a second flight, entangles the two at the propagation (#3443). The second flight only propagated pending onto the memo — no recompute, its inputs' values were unchanged — so the memo's stamped re-entry never ran and the first transaction never learned it was waiting: it revealed `A: 1` beside the committed `Sum: 0`, and `Sum: 2` arrived with `B: 1`. Now one reveal when both have landed (A15). A render effect reading both plainly stays parallel, as before; a write whose async work flows into a held memo is held with it. + - A zombie dirtied through the lane channel runs instead of being cancelled (#3444). When the parking batch is the transaction, queued zombie recomputes are cancelled as a world the zombie never displays — but overrides and `latest()` companions are the mainline frame, and the still-visible branch showed `latest(count)` at 0 beside the same read outside at 1. + +- 9da7f0a: `isPending(details)` reports the load of an optimistic value when `details` derives it through an async memo (#3379). `notifyStatus` now assigns the node's optimistic lane before poking its companions, so a companion lane created by the poke is parented to the node's lane: the indicator effect flushes on the companion's child lane immediately instead of merging it into the held lane and waiting on the async it reports. +- 62b0a22: A render effect that stops reading a pending async memo no longer keeps the memo's source held. A pending reporter that recovers without its flight landing — its pass no longer reads the source, e.g. a `show()` gate closed — is a completion event for the transaction it reported to: `recompute` now wakes that parked transaction (`wokenTransitions`, the third site after disposal #3372 and boundary reset #3375) so it is re-judged and its held writes commit. Before, `reporterBlocksSource` already judged the effect dead but nothing re-asked the parked transaction, and an ordinary write to the source stayed staged for as long as the flight stayed up — forever when it never landed. Found by the semantic fuzzer (#3446, law P1, 21/1000 cases) and reproduced as the posture matrix's `effect × gatedAway` cell. +- 27b24aa: Five hold-consistency fixes (#3456, #3458, #3460, #3463, #3469) + - #3456: a pass that re-parks on a new pending source set retires the sources it stopped carrying from its dependents, so a conditional whose async branch was cancelled no longer stays pending forever on a flight it has no path to. + - #3458: a stale render reader that is a flight's first observer registers the flight with the transaction it reveals a reader of (INV-3, via the reader's queue chain), so the transaction waits for it instead of revealing its other inputs beside the reader's pre-flight value. + - #3460: lanes mirror transitions from the outside — a render effect off a held lane (mounted mid-hold, or re-run by an unrelated sync write) is served the committed value, publishes at once, entangles nothing, and re-runs at the lane's release; `latest()` and `createOptimistic` sources alike. Only direct reads return the override while the lane holds. Off the lane is provenance, not membership: a pass under the lane's own transaction (its async's landing) is the lane's work. + - Lanes stage (#3479 review): an optimistic derivation is an override. A lane pass on a memo publishes its speculative result as a _derived override_ instead of direct-committing `_value`, so the committed view an outsider sees is a whole frame — the held source and its derivations together, never a committed shadow beside a speculative memo. The revert promotes a derived override the truth confirmed (no re-ask waterfall) and re-derives one it superseded. + - #3463: a reader whose removal is staged in a live transaction (a zombie) is still on screen and keeps holding until the commit that disposes it; it is moot only for that transaction's own verdict. + - #3469 (A30): a pass that changed nothing replaced nothing either — its dependency trim waits on the flush's verdict (`heldTrims`), so a same-value branch switch under a hold still follows its committed inputs. + +- 549f482: Trim per-node work on the hydration claim path. `gatherHydratable` asks once whether the root contains frame regions and tests containment against that list, instead of walking every keyed node's ancestor chain with `closest("[data-fid]")`; `insert()` builds a parent's claim array in one indexed pass over `childNodes` that drops separators as it copies, instead of an iterator spread followed by a compacting pass; and `clearSnapshots` assigns `undefined` to the extension's `_snapshotValue` rather than `delete`-ing it, which pushed every hydrated source's extension object into dictionary mode. +- d80cd1f: `isPending()` on an optimistic node whose own source landed a value differing from the displayed override now reads `true` even when the node has never committed (its first landing was held by a reveal that never landed). The verdict's uninitialized suppression — A19 exception (1), "no observable value exists" — no longer applies under a displayed override, which is an observable value (A18 d). +- 76230f9: Report `isPending()` true for a synchronous memo held by a downstream async memo. The memo's staged recompute only refreshed its verdict companion inside an active transition, but a plain flush can become a hold after that recompute (an async memo pends and the batch is adopted into a transaction), so the memo read not-pending while the signal it derived from read pending (#3413). +- 347a5ca: A memo wrapping `isPending(x)` agrees with a direct `isPending(x)` read while a sibling async memo holds the write (#3457). + - The fresh-read pairing rule (A10) only mutes a verdict for a LANDED answer awaiting reveal; while the transaction still has an async source computing, pending is the verdict for every reader. That carve-out was gated on the node's `_transition` stamp, but a sync memo staged AFTER the transaction opened is pushed straight into the transaction's batch and is not stamped until the flush stashes the hold. A wrapper memo recomputing on the companion flip read the memo's fresh staged value mid-flush, was told "not pending", and cached `false` for the whole hold, while the direct render-effect probe (which reads the committed value under the companion lane) reported `true`. The scan now runs for an unstamped node too: the transaction it resolves to is the one that owns its staged write. + +- a8a8949: `latest(() => store.key)` on a derived store (projection) that has not yet resolved threw for tracked and untracked reads but returned the **seed** through `latest()`: `read()` routes a `latest()` read to the companion before its firewall/status logic, and the leaf's own `_value` is the seed. `latest()` now judges "uninitialized" on the leaf's owner — the projection's firewall — and throws `NotReadyError` like every other read (A25: the seed is a draft, never a value; A7). +- 632e45c: Give the latest() shadow companion the `ownedWrite` flag its isPending companion already carries (#3378). A companion sync is internal plumbing that can run from inside a computation — a transition-held memo recompute pulled mid-tick by a reader creating or refreshing its latest() shadow — and the dev owned-scope write guard halted the app on the shadow write. Toggling a JSX branch that reads `latest(memo)` off while an action is pending and restoring it as the action resumes threw REACTIVE_WRITE_IN_OWNED_SCOPE. +- 75c5113: `latest()` of an uninitialized async source now throws `NotReadyError` in every scope. Unowned callers (event handlers, imperative code) used to receive `undefined` — a value the accessor's type excludes — because the uninitialized case shared the pending-shadow fallback's condition in `latestRead`. `isPending()` is unchanged: an unowned probe still answers `false` (A16), which `boolean` admits. Spec: A7 amended, A16 wording corrected (the boundary is ownership, not tracking), A17 authoritative-reader carve-out ruled, A32 added (children-forbidden readers see the frame). +- 899c2c4: `merge()` and `omit()` are always lazy views, and props consumers read their leaves + + `omit(props, ...keys)` returns a live view of `props` for every input — a plain object included — instead of copying it with a `getOwnPropertyDescriptor` + `defineProperty` per prop. A predicate form hides keys by rule without enumerating first: `omit(props, k => k[0] === "$")`. `merge()` no longer builds an eager copy when its sources are plain objects: under `Proxy` it always returns an O(1) view over the flattened sources (a single non-function source is returned as is). + + The two compose flat. An `omit()` over a `merge()` carries one filtered view per flattened merge source, a `merge()` over an `omit()` takes the view record as a leaf, and nested omits fold their filters into one record. A component chain of `merge(defaults) → omit(consumed) → merge(statics) → omit("as")` — the shape headless-UI libraries render every element through — collapses to leaf views over the original objects, each with its accumulated filter, with no proxy layer left between the outermost spread and the author's props. `merge()` keeps the omitted keys hidden by construction (#3014) rather than by treating the omit as opaque. Construction cost drops 3–7× at depth 1–7; the SSR polymorphic-chain bench (#3448) runs ~2.4× faster. + + Reads stay cheap: a view over plain objects resolves a key → owning-leaf table once, on first read, and every `get`/`has`/descriptor is one lookup after that. `spread()` (DOM and universal) and `ssrElement()` read the leaves directly — never through the proxies' traps — and walk that table when there is one, so an effect rerun costs one read per key, as it did over the copy. Both proxies use a class target and one shared handler (no per-instance closures). + + A view over a store asks the store nothing but the read. Each source's kind (plain object, omit record, proxy, memo) is decided once, when the view is built, and carried beside it — every brand check on a Proxy is a trap (`instanceof` is a `getPrototypeOf` trap, as expensive as a store read), and store detection goes through `$TARGET`, a symbol the store's `get` trap answers on its fast path, never its generic tracked-read path. `merge(defaults, store)` constructs ~30% faster than the copy did and reads ~15% faster; `omit(store)` reads at parity. + + The views tell the truth: `Object.getOwnPropertyDescriptor(view, key)` reports a data descriptor only when the key is a data property of a plain leaf (the compiler's encoding of a static prop) and an accessor for a getter, a store key, or a memo source. Together with the new internal `hasStaticKeys()`, `spread()` now skips the children effect for static children behind `omit`/`merge` layers (#3388 through views). + + Behavior changes: + - Writes to a `merge()` or `omit()` result are no-ops (they already were for the proxy forms). A caller that needs its own object copies it (`{ ...merged }`), and the copy carries no sources (#3384). `@solidjs/html` now collects its own props and spreads into one `merge()` at the end instead of assigning onto the result. + - A data property on a source is read live through the view rather than snapshotted at `merge()`/`omit()` time. + - Key order of a merged view is the merged order — every key at the position of the last source that carries it — matching `ssrElement`'s array form. + - Sources are treated as own-keyed; a key added to a plain source after merging is not seen (the copy did not see it either). + - Enumerating a view through its traps (`for…in`, `Object.keys`, `{ ...view }`) costs a trap per key, as any proxy does; the internal consumers avoid it. Environments without `Proxy` keep the copy paths. + + Internal helpers for consumers, exported from `solid-js`: `viewOf(o)`, `mergeView(o)`, `omitView(o)`, `sourceKeys(entry, kind)`, `sourceHas(entry, kind, key)`, `sourceGet(entry, kind, key)`, `hasStaticKeys(o)`, `resolvedTable(o)`, the `SOURCE_*` kinds. + +- ca05917: `OBSERVE.exclude` now covers writes: a root write to an excluded subject (the observer's own store or signal) no longer counts toward the interaction that made it, and an interaction whose writes all went to excluded subjects with none of the app's work run — a click on a devtools panel's own button — is not recorded. Store nodes carry the owner their store was created under, so an excluded panel's store is an excluded subject like its signals. +- 3ae9e92: `OBSERVE.records` — one records channel on both platforms (observe/dev tiers); frame records from both ends; the client `"call"` record; `observeServerFunctionCalls` removed + - **`@solidjs/signals`**: `OBSERVE.records` — `subscribe(type, listener)`, `observed(type)`, `emit(type, event, live)` — the channel every runtime record rides, created once per process and registered on `globalThis` under `Symbol.for("@solidjs/signals/observe/records")` so a second copy of the core (a bundled server build instrumented through `--import`) and wire layers bundled without a framework import reach the same listener sets. Listeners are snapshotted per emit; a throwing listener is reported and the rest run. Types: `Records`, `RecordTypes` (extends `HostRecordTypes`; both declared empty, for the runtimes to augment — one augmenter per interface), `RecordType`, `RecordEvent`, `RecordLive`, `RecordListener`. Folds out of prod. New **`OBSERVE.attribution.currentOrigin()`** (and the `currentOrigin` hook on `AttributionHooks`): the provenance a root write performed now would be stamped with — the interaction whose handler is running, the navigation/effect/action frame open, or inside a recompute the origin of the change that caused it — as the engine's own `ChangeOrigin` object, `undefined` when external or with no engine; for a runtime stamping a record of its own. The installed hooks are also registered on `globalThis` under `Symbol.for("@solidjs/signals/observe/attribution")`, the same reach-without-an-import the channel has. + - **`solid-js`**: the `"boundary"` record moves from `OBSERVE.server.records` to `OBSERVE.records` (augmenting the core's `RecordTypes`). `OBSERVE.server` keeps only the `trace` slot; `ServerRecords` is gone. + - **`@solidjs/web`**: the `"invocation"` and `"frame"` records move to `OBSERVE.records` (augmenting `HostRecordTypes` through `solid-js`). New **`"call"` record** (`CallEvent`, `CallLive`, `CallListener`): one per server-function call made from the browser, at the caller's settle — `{ id, at, durationMs, method: "GET" | "POST", outcome, status?, origin?, deferred? }` with `{ args, response?, result? | error? }` beside it; joins the server's `"invocation"` by `id`, and — through `origin`, the engine's own interaction/navigation object read at dispatch via `currentOrigin()` — the attribution engine's `InteractionEvent` / `NavigationEvent` / `HoldEvent` by identity, so an observer files the call under the click that made it without a time join. The **`"frame"` record now has a client half**: `FrameEvent` is `FrameProducedEvent | FrameAppliedEvent`, discriminated by `side`, same census on both; the client half (`applyFrameResponse`, one per stream in a response) adds `address` (the `as` remap) and `outcome: "truncated"` for a body that ended before `complete`, with `live.response`. Server census fix: `regions` counts `html` chunks addressed to a child frame id (the former count read a chunk type that does not exist), and `shellMs` is set by the stream's own shell only. The emitters and their wrappers fold out of the prod client artifacts behind the observe literal (prod `applyFrameResponse` and the server-function dispatch are the pre-existing functions, no extra frame or promise hop). The server-functions and frames **client** entries gain `observe` and `development` builds and export conditions (`server-functions/dist/client.{observe,dev}.js`, `frames/dist/client.observe.js`); the server-functions client is now built with its flags replaced in every tier (before, `_SOLID_DEV_` there was an unreplaced truthy string). + - **Removed**: `observeServerFunctionCalls` and the `ServerFunctionCall` / `ServerFunctionRequestCall` / `ServerFunctionResponseCall` types, from both server-function entries. Subscribe to `OBSERVE.records` `"call"` (client) or `"invocation"` (server) instead. + - **`@solidjs/diagnostics`** (format v6): `artifact.server` is replaced by `artifact.records: { boundary, invocation, frame, call }` — always present, captured on both platforms including the browser bridge; types `BoundaryRecord`, `InvocationRecord`, `FrameRecord` (`FrameProducedRecord | FrameAppliedRecord`), `CallRecord` (with `origin?: ChangeOrigin`), `ArtifactRecords` replace the `Server*Record` / `ArtifactServer` names. JSONL: one line per record with `type` naming its table; the meta line's `boundaryCount`/`invocationCount`/`frameCount` become `recordCounts: { boundary, invocation, frame, call }`. + +- 328580f: `omit()` over a `merge()` is one record that holds the merge's record, not one leaf view per merge source + + An omit over a merge used to flatten at construction: one `OmitView` plus one combined hidden-key list per flattened leaf, and the next `merge()` copied those entries into its own arrays. On a component chain of defaults + omit + spread (Kobalte-shaped: `merge(omit(merge(omit(props))))`, four layers) that was ~19 records and as many list copies per element — the largest allocation of the render. The omit now holds the `MergeView` record itself (a new source kind, `SOURCE_MERGE`) and is one record however many leaves the merge has; a later `merge()` carries it as one entry, and a later `omit()` folds into it. Nothing is read through a proxy trap along the way: the entry helpers (`sourceKeys`/`sourceHas`/`sourceGet`, `hasStaticKeys`, descriptors, the resolved table) recurse into the record by function call. + - Reads through the nesting are one walk per read (a nested entry answers presence and value together), and a nested record counts no reads of its own toward the table threshold — the view that was asked decides for the whole tree, and its table is collected in one pass over the leaves rather than one table per layer. + - New `@internal` `sourceOwners(source, keys, owners)`: every key of a props source — a plain object, a store, or a merge/omit view — appended in merged order with the object that owns it, in one pass, later sources moving a key to the end. `ssrElement` collects any spread that is not plain objects only (a view, a store, the array form with one among them) this way, so each attribute is one direct read of its owner — no `in` walk per key through the layers, no key list per leaf, no table, and no per-entry classification (`pushEntry` is gone). + - An omit's `$SOURCES` never answers anything now (previously its filtered leaf views); consumers reach the record through `viewOf` and walk it as one filtered entry. + + Measured against `next` (interleaved, min of N, quiet machine): the tier-1 polymorphic-chain SSR harness allocates 12% less per row (14.6 → 12.8 KB) and is 2–6% faster across the interpreter, Sparkplug, Maglev and TurboFan tiers; the props-chain microbench builds 6–65% faster and builds+consumes 7–31% faster by depth and tier; the omit/merge micro-suite is flat or better in every shape. On the yak-bench SSR lanes with every yak piece on Solid primitives: +7% geomean, +20–36% on the component-composition cases (`polymorphic-chain`, `tabs`, `multifile-composition`), which brings those to parity with yak's hand-rolled runtime. + +- 0bffee2: One definition of "unflushed" for signals and store leaves. A staging adopted by a transaction before any flush carried it (`set(x, 1); action(...)` in one tick — same-tick adoption is by design) is still unflushed whatever its stamp: `latest(x)` answers the pre-write value and `isPending(x)` false inside the adopting action's body, as store leaves already did through their own selection while signals answered the staged value and `true`. `initTransition` marks such nodes at adoption (`CONFIG_ADOPTED_UNFLUSHED`); the carrying flush clears the mark and the hold takes over. +- f329a26: One ownership relation, `ownsHold`, answers "is this hold part of the running pass's world" for the stale-reader clause, the lane arm and the store's backing holds — a refactor with no behavior change, recording the ruling that a lane is a transaction with an override whose world includes the transition that owns it. +- c827758: A projection's leaf companions die with the projection. Disposing a store whose async source was mid-refetch left the `latest()` shadow of a leaf orphaned: never derived (its compute read through the projection in flight, the backfilled override stood in), its override dropped at the settle, NotReady forever against a leaf whose committed value differed — the `__TEST__` quiescence invariant INV-4 at the next flush. Externally coherent, but a companion outliving its source. The firewall's teardown now snaps the companions of its companion-bearing leaves (the shadow is retired; a later read recreates it from the committed view), and `latest()` of a leaf whose firewall is already disposed serves the committed value without creating a shadow — a boundary's content re-running after the teardown had recreated one that nothing would retire. Surfaced once #3495 stopped leaking parked transactions, which had masked every quiescence check in the posture matrix; all 621 matrix cells now run with zero invariant violations. +- 6e9243c: fix(signals): a projection leaf released by the unobserved sweep also leaves its firewall's companion set, so an obsolete value read only through `latest()`/`isPending()` becomes collectable once every reader is disposed (#3503) +- 7a09cd9: One slow value selection, `serve`, for signal reads and store property nodes (the fast paths keep their inline ternary). Fixes a derivation's untracked read of a derived optimistic store's key after its own truth landed differently from the optimistic edit: the store served the memo the superseded override and let it publish, where a signal serves the landed truth and holds the memo with the action (A18). +- 61a114c: Server boundary records on `OBSERVE.server.records` (observe/dev tiers) + - New `"boundary"` record: one per `` boundary that waited during a server render, delivered when it settles — `{ id, at, durationMs, heldMs, passes, outcome: "settled" | "fallback" | "client" | "error", streamed, revealGroup?, ownerPath? }`, with the thrown error beside it. `id` pairs it with `SSR_RENDER_ERROR_CONTAINED`; `passes` counts render passes (a sequential chain reads as `3+`); under a `` group the record waits for the group's swap so `heldMs` measures how long finished content was held for its siblings. No clock is read without a listener, and the emitter folds out of prod. + - The server records channel is `OBSERVE.server.records.subscribe(type, listener)` — the server twin of `OBSERVE.attribution.subscribe(type, …)`. `OBSERVE.server.invocations` (unreleased) is renamed onto it: `subscribe("invocation", …)`. The `InvocationChannel` type is gone; `ServerRecords` is the channel's interface. + - Types now layer one augmenter per interface: `@solidjs/signals` declares `ServerObserve` empty; `solid-js` augments it with `records: ServerRecords` and `trace: ServerTrace`, declaring both; `@solidjs/web` augments those two through `"solid-js"`. `TraceSlot` in `@solidjs/web` is now an alias of `solid-js`'s `ServerTrace`. (Two augmentations of one re-exported interface through different module aliases merge order-dependently in TypeScript — one set was silently lost.) + - `@solidjs/signals` exports `ownerPath(subject)` — the root-first component-label walk its diagnostics already make — so a record's `ownerPath` and the finding it pairs with come from the one walk. + +- 0d8347a: Server records reach the diagnostics artifact and the dev checks (server-dev-build-plan P4) + - `@solidjs/diagnostics` artifact format **v5**: `artifact.server: { boundaries, invocations } | null` folds `OBSERVE.server.records` when the scenario runs under the server runtime — `captureArtifact(() => renderToStream(…))` — one row per `` boundary that waited and per server-function execution; `null` for client captures and the browser bridge. New exported types `ArtifactServer`, `ServerBoundaryRecord`, `ServerInvocationRecord` (mirrors of the runtime's `BoundaryEvent`/`InvocationEvent`; the package still depends on `@solidjs/signals` alone). JSONL egress adds `boundary` and `invocation` lines and the header counts. + - `InvocationEvent.boundary`: a direct server-function call made during a `` boundary's render pass carries that boundary's hydration id, the `"boundary"` record's `id` — the join between a boundary's wait and the calls under it. + - Two dev checks derived from the boundary facts in `ssrLoadingBoundary`: `ASYNC_WATERFALL` with `data.side: "server"` (`passes - 1` sequential flights; 2 → `info`, structured only; 3+ → console `warn`) and a new code `SSR_CLIENT_CONTENT_MASKED` (`warn`, `ssr`) for client-only content that surfaced only after a real server wait — the server's work discarded, the fallback shown for the wait. Dev tier only; the boundary clock now runs in dev without a listener. + - `solid-js`'s server `emitFinding` keeps `info` findings off the console (structured channel only), matching the core. + +- 7623ce1: Server diagnostics on `OBSERVE.diagnostics`; `OBSERVE.server` owned by `solid-js`'s server entry + + The server runtime now reports on the same structured channel as the client. **Findings** — facts about a render, present in observe and dev builds — `SSR_RENDER_ERROR_CONTAINED` (a render error a boundary routed; `data.handling` is `fallback`, `client`, or `failed` — the structured face of what `renderToStream`'s `onError` receives, on the process-wide channel), `SSR_SUBTREE_ABANDONED` (a failed fragment's pending descendants discarded), `SSR_STREAM_ABANDONED` (consumer cancelled or sink failed mid-render), `LATE_HEADER_WRITE` (recorded beside the existing dev throw / prod log), `SERVER_FN_ERROR_SANITIZED` (the original error the production wire replaced), and `FRAME_MARKER_CORRUPTED` from the frames client. **Checks** — dev-only guidance — convert every server `console.warn` to a code: `SERVER_WRITE`, `REVEAL_IN_RENDER_TO_STRING`, `LAZY_ASSET_UNMAPPED`, `PRELOAD_DESCRIPTOR_INVALID`, `HEAD_TAG_INVALID`, `BEHAVIOR_CLAIM_DROPPED`, and `UNRECOGNIZED_INSERT_VALUE` (now one code and a `render` kind on both platforms); `ASYNC_OUTSIDE_LOADING_BOUNDARY` on the server records with `data.side: "server"` before it throws. Server components are labelled for `ownerPath` (`createComponent` runs the body under a transparent `` owner in observe/dev — no hydration id consumed), so `in ` reads the same on both sides, and the server entry installs the same repair-guide console footer as the client. Prod artifacts carry none of it; `DiagnosticKind` gains `ssr`, `head`, `render`. + + `OBSERVE.server`'s objects (the invocation listener set, the trace-provider slot) are now created by `solid-js`'s server entry, once per process under `Symbol.for("solid-js/observe/server")` on `globalThis`, rather than by `@solidjs/web`'s module init: an observer's `init()` that imports only `solid-js` can subscribe and `provide` before the web runtime has loaded, and a host that bundles the runtime into its server build and instruments through a `--import`ed module finds one listener set and one provider across both copies. The core keeps `server: {}`; the client pays nothing. + +- af94f67: Server observe surface: `OBSERVE.server` and the invocation channel + + `OBSERVE` gains a `server` slot — an augmentable `ServerObserve` interface declared empty in `@solidjs/signals` (re-exported by `solid-js`), typed and emitted into by `@solidjs/web`'s server runtime, so server-side observability consumers subscribe on the one `OBSERVE` object they already know from the client. The first channel is `OBSERVE.server.invocations`: `subscribe("invocation", (event, live) => …)` delivers one `{ id, direct, at, durationMs, outcome, deferred? }` record per server-function execution — HTTP dispatch and direct SSR calls alike — when it settles, with the request event, `request`, `args`, and the result or the error as thrown beside it. Observers, not policy: any number of listeners, none able to alter the call; `wrapInvocation` remains the single policy hook. + + `@solidjs/web` now publishes observe-tier server artifacts (`dist/server.observe.js`, `server-functions/dist/server.observe.js`, `frames/dist/server.observe.js`) under the `observe` export condition, alongside the existing dev/prod pairs. The surface and every emit site fold out of the prod artifacts. + +- e87d694: `{ shallow: true }` computed stores keep their leaves raw on every path (#3498). The projection draft no longer wraps a nested value in a draft proxy — leaf identity holds and a frozen leaf can no longer trip a Proxy invariant — and the loading shadow, its commit copy, the SSR draft and snapshots, and the hydration replay shadow copy only the root container instead of JSON-cloning the tree, which turned `Date` into a string, `NaN` into `null`, dropped `undefined` properties, and could not represent BigInt or cycles. Deep stores are unchanged. +- dd19e9e: Consolidate value-selection predicates: `readerSeesCommitted` (the full committed-vs-staged arm read()'s slow tail used to inline) and `visibleOverride` / `hasActiveOverride` (one definition each, previously duplicated between the core, lanes, verdict channels and the store) — a zero-semantic-change refactor toward one implementation per rule (DESIGN-CONSOLIDATION, move 3b). +- c245532: Spec: A33 records the fallback-hold ruling — a flight observed only behind a `` fallback holds no transaction (#3375), and an `on` reset moves the hold onto the boundary rather than ending it (#3459). Comment-only citations at the two sites; no runtime change. +- 34287d8: SSR render failures reach the client sanitized (#3468): the wire policy the server-function handler has applied since #3113/#3116 now covers every SSR road a failure takes — the error an `` serializes for hydration, a rejected async source serialized into the stream, a `` fragment's `_fr` rejection, a frame stream's error chunks (the fragment's, a live hole's, the root's). Outside the dev build a plain thrown value is replaced with a generic `Error` (`"Internal Server Error"`); `message`, `cause` and own properties stay on the server. A `"use server"` function called in-process during SSR never touches the RPC wire, so before this the production page load shipped what that wire withholds. + - **`solid-js`** (server): `` sanitizes _before_ rendering its fallback and serializes the same replacement, so fallback markup and the hydration record agree. A fallback printing `err().message` shows the generic message in production, as for a server-function failure. `markSafeError` (`Symbol.for("solid.SafeError")`) passes through with own properties; an Error reached as a _value_ is data and passes as written (#3113's ruling). One replacement per original, however many roads it takes. New finding `SSR_ERROR_SANITIZED` (`info`, observe + dev; `data.error` the original), beside `SSR_RENDER_ERROR_CONTAINED` which carries the failure itself. `ssrSanitizeError` is exposed to the runtimes through `solid-js/internal`. Server findings now carry their `ownerPath` in the **observe** artifact too — the core's walk reads `_parent` under its own build's property mangling and found nothing on a server owner there, so the server entry locates its findings itself. + - **`@solidjs/web`** (server): the hydration serialize funnel guards every channel it writes (a promise's rejection, an async iterable's thrown step); a fragment's terminal error reaches the `_fr` rejection and a transport sink's error chunk sanitized while the abandonment ledger keeps the original; the frame sink's root error chunk and the live-hole/live-attribute error chunks carry the replacement's message. + - **`@solidjs/signals`**: the `SSR_ERROR_SANITIZED` code. + + The dev/prod line is the build variant: the `development` server artifacts keep full fidelity; production and observe sanitize. + +- 64f9266: A render effect that stops reading a pending memo no longer keeps the memo's source held, in every ordering. Reporter liveness now reads this pass's deps — `reporterBlocksSource`'s scan stops at `_depsTail` instead of walking the committed frame's deps that A30 keeps linked until the commit trims them (a staged pass that had stopped reading the memo still looked live through its kept dep, and the hold it kept was the commit that would have trimmed it). A reporter retires when its pass drops a dep — not only when it recovers from pending, which a reporter registered by the stale-reader carve-out never was — and the retirement wakes every parked transaction rather than the reporter's stamp, since the transaction waiting on it registered it without stamping it. Semantic fuzzer (#3446), same campaign: 994 pass / 0 fail / 6 policy, from 984 / 4 / 12. +- 0148d58: A render effect's untracked read of a store key held by a foreign action — a key with no node, or a `reconcile` adoption held by the action — is now recorded for replay at the action's commit, as the signal path always was. Previously the store's backing-level selection served the committed value but skipped the registration, so the effect stayed on the pre-action value after the action settled. One registration (`recordStaleReplay`) is shared by the node path and the store's backing paths. +- 765a656: `Object.getOwnPropertyDescriptor` on a store is now reactive: the descriptor trap subscribes to the key's presence node and witnesses `isPending()` / `affects()` as `in` does. Previously a render effect that inspected a key through a descriptor never re-ran for an optimistic add or delete, and an `isPending()` probe over it saw nothing. +- 9db33cf: The store's backing-level visibility (which container — committed or staged — a reader of a held store sees, for property reads, `in`, `Object.keys` and descriptors) is one `holdVisible` on the core's shared predicates for both hold kinds (a setter's fold, an adoption under a transaction), replacing six store-local helpers. No behavior change; −313 B minified in the store. +- bfd6f6c: Store node reads select committed-vs-staged by the core's `readerSeesCommitted` (one Rule 1 implementation for signals and store nodes). Fixes a render effect's untracked read of a store key held by a foreign action never replaying at that action's commit — the store's hand-restated stale-of-foreign clause served the committed value but skipped the replay registration the signal path performs, leaving the effect on the pre-action value permanently. +- 2054045: An optimistic store override now survives its key becoming unobserved. The property node was released with the override on it the moment its last reader left, so an untracked read of the key (`s.n`) returned the committed value while the action was still live; the release now waits for the flush that resolves the override, as an optimistic signal keeps its override whether or not anything reads it. +- c410709: An optimistic add or delete on a store now survives its only structural observer leaving: the key's presence node was released with the membership override on it, so `in`, `Object.keys` and property descriptors fell back to the committed structure while the action was still live. The release now waits for the flush that resolves the override, as the value slot's does. +- f555ec2: The store setter's owned-scope write guard no longer exempts roots (#3500). A `createRoot` body is tree construction — every dev component body, every context Provider, the top of `render()`, and the whole SSR pass run directly under one — so a store write there is a write in an owned scope, exactly as `setSignal` has always treated it. Dev/test builds now throw `REACTIVE_WRITE_IN_OWNED_SCOPE` for store writes in root and component bodies that previously passed silently; production is unaffected (the guard is dev-only). + + `OBSERVE.exclude`: the `IMMUTABLE_UPDATE_IN_STORE` census now names the store's own owner as its subject rather than the writer's context, so an excluded panel's store stays silent however its writes arrive. The docs no longer suggest `runWithOwner(panelRoot, …)` for panel writes — that is a write in an owned scope. + +- d8e35a3: A memo or user effect created on mainline whose untracked read (`untrack(() => s.n)`, `deep(s)`) is of a store key held by a live action is now born held (A29), as the same read of a signal is: the pass enters the action's transaction and publishes nothing until the action commits. Previously the store's untracked paths served the held value without entering, so a mainline memo published the action's unrevealed write to the screen. Covers keys with and without a node and `reconcile` adoptions held by an action. +- 5f7da9d: A transaction blocked on a memo's flight stays blocked while an upstream re-ask supersedes that flight (#3462). + + `transitionComplete` judged a reporter's source by its own flight alone (the self entry in `_pendingSources`). A re-ask upstream retires that entry and leaves the source pending on the new flight instead, so the verdict flipped to "complete" while the source's reader still could not render. The transaction was parked, so nothing re-judged it until a re-entry: repeating `setShow(true)` while the first write was held re-entered it, and the flush committed `Show: true` beside `Panel: hidden`, with the panel catching up seconds later at the chain's landing. The source now blocks while it is pending on anything; the landing folds the transaction in as before (A15), and without the repeated write the frames are unchanged. + +- 7f5f902: `merge()`/`omit()` views build their resolved key table on enumeration or once reads have paid for it, not on the first read; `ssrElement` walks a view's entries instead of asking for its table + + A `merge()`/`omit()` view over plain objects keeps a resolved table — every key mapped to the leaf that owns it — so a client `spread` rerunning its effect, or `Object.keys`/`{...props}`, is one lookup per key. Since the views became lazy (#3454) that table was built by the **first** per-key trap read as well. On the server that is the wrong trade: a component reads its merged props a few times, the element serializes them once, and the view is discarded, so a Kobalte-shaped component chain (`Dialog.Trigger` → `Button.Root` → `Polymorphic`) paid for a table per layer per element. Profiled under `renderToString`, a third of the time was in the table code (`mergeTable`, `tableSet`, `omitTable`) and the garbage it produced, plus `Array.prototype.concat` combining omit filters. + - **Signals:** a per-key `get`/`has`/`getOwnPropertyDescriptor` answers by a source walk (last source first, one `in` each) until the view has been read 16 times — the break-even between a build (~60 ns per key of every leaf) and a walk (~20 ns per source) — and then builds the table as before, so a long-lived client view read on every reactive rerun is one lookup per read from its first few updates on. Enumeration (`ownKeys`, or a consumer asking `resolvedTable`) builds it outright, unchanged. An omit over one object never builds one. The read count lives in the table slot until it is decided, so a view carries no extra field. Combined omit filters are copied with `slice` + `push` instead of `concat` (2–3× cheaper once optimized, and — unlike a hand-written loop — no more expensive than the builtin in the interpreter and baseline tiers, which is what an instruction-count benchmark under Valgrind mostly runs). + - **Web (SSR):** `ssrElement` no longer prefers a view's resolved table; it walks the view's entries the way it already walked the array form — an omit over a merge as its filtered leaf entries — so serializing an element builds no table at all. Attribute order is the same merged order (a key at the position of the last source that carries it). + + Same-process A/B on the tier-1 `polymorphic-chain` SSR bench (200 rows, `renderToString`): the chain form goes from 8.2× the compiled floor to 5.8×, chain-static from 7.7× to 5.6× (−27% wall-clock). At the signals layer the props-chain bench improves 5–17% on build, 6–14% on build+consume, and steady-state per-key reads on a prebuilt view are unchanged (the table is in use). The only slower band is a view read 16–20 times and never enumerated (+4–17% at depth 1–3, faster at depth 7), which is the transition the threshold is designed around. + + Tests: the table is undecided after a handful of reads on both a merge and an omit-over-merge, built after the 16th with identical answers before and after; enumeration builds it on a fresh view; a plain omit never builds one; a store-leaf view settles to "none" and keeps walking. SSR: a spread over an omit-over-merge and over a bare merge serializes the merged order and builds no table. + +- 31adfce: A write is a proposal (A34, #3494). A mainline write to a node a transaction holds — the same value again or another — is a second proposal for the same slot: the writer's tick joins the hold at the next flush's start and reveals with it (`setA(1); setB(1)` with `b=1` held holds A with B again; #3473 had dropped the entry, and with it the grouping, while fixing its `activeTransition` leak — the entry is now deferred instead). A tick whose writes net to the committed value proposed nothing: the node is not staged, not stamped, and not pending (`setShow(false); setShow(true)` beside a held `setCount(1)` no longer reads `isPending(show)` true nor captures the later `setShow(false)` into the hold — the hide that used to be lost). Fixes the torn `[1, 0, 1]` effect input from #3473's review. Also: a stale reader served a held memo's committed value counts as observing the memo's flight (a reveal in the flush that retires the flight's last reader holds the transaction — `Count: 1` no longer publishes beside a visible `Copy: 0`), and a `latest()` shadow backfilled under a transaction never blocks its settle (removing the last reader releases the held write at once). From differential fuzzing of the ruling and review: a source going pending behind a memo's kept dependency tail now re-derives the memo instead of marking it pending (A30 amendment) — a reader that stopped reading a memo is no longer registered on the memo's next flight (which held an action's truth on a fetch nobody displayed), and a memo whose committed frame still derives from the source no longer publishes stale beside the source's new inputs (`1 0` with `selected` derived from `remote(0)`); the no-proposal drop applies to unstamped nodes only — a held proposal rewritten to the committed value stays its transaction's, so the authoritative value that follows commits instead of being skipped as another transaction's — and covers writable memos (`createSignal(fn)`) as it covers signals; a same-value write to a held node schedules its own flush, so the join never leaks into a later, unrelated tick; and the join drain runs inside the flush's guard, so a throwing comparator cannot wedge the scheduler. + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/signals/package.json b/packages/signals/package.json index 410c17506..029db5ee5 100644 --- a/packages/signals/package.json +++ b/packages/signals/package.json @@ -1,6 +1,6 @@ { "name": "@solidjs/signals", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "description": "Solid's reactive primitives: signals, memos, effects, stores, and async-aware computations.", "author": "Ryan Carniato", "license": "MIT", diff --git a/packages/solid/CHANGELOG.md b/packages/solid/CHANGELOG.md index 76eb3b3ba..8d3c9b9d6 100644 --- a/packages/solid/CHANGELOG.md +++ b/packages/solid/CHANGELOG.md @@ -1,5 +1,213 @@ # solid-js +## 2.0.0-rc.9 + +### Patch Changes + +- 8bf04ea: New dev diagnostic `ASYNC_STORE_SETTER`: a store setter callback that returns a Promise now throws in dev (through the diagnostics channel) instead of being silently ignored. A store setter is a synchronous transaction — the draft closes when the callback returns — so `setStore(async d => …)` committed only the writes before its first `await` and lost the rest. Covers every store family with a user setter (`createStore`, `createOptimisticStore`, the derived store's manual setter); a derived store's own async compute is not affected. Store-specific: a signal may legitimately hold a promise, so `setSignal` has no such rule. Production is unchanged. + + Docs: the `OBSERVE.exclude` guidance in RFC 08 no longer suggests `runWithOwner(panelRoot, …)` for panel writes (that is a write in an owned scope, see #3512); the store's own nodes carry the excluded owner. + +- d826cd3: The client error hook — `configureClientErrors({ onError })` from `solid-js`, and `render`/`hydrate`'s `onError` option in `@solidjs/web`: the prod-tier seam through which an app, or an error monitor, hears the one failure nothing else can see — an `` / `createErrorBoundary` collected it and renders its fallback. The twin of the server's `configureServerErrors`. An uncaught error is not this hook's: the halt (`REACTIVITY_HALTED`) hands its cause to the platform's `reportError`, the channel every monitor already listens on. + + Once per error object (a `reset()` re-collecting the same failure says nothing new; a primitive is reported per sight); `ownerPath` carries the component labels where the runtime keeps owner names; no return — the client has no wire to map for; a throwing hook is reported and ignored. A root's own hook wins over the ambient one for failures under it. + + Pay-for-use: the hook machinery (`core/error-hooks.ts`) is retained by `createErrorBoundary` or the app's own `configureClientErrors` import; a root's hook is parked on the root owner under the registered `ROOT_ERROR_HOOK` symbol (defined in the scheduler), so `render` retains nothing for an app that passes none. Core floor unchanged; apps with a boundary +~200 B. + + **The server surface consolidates on the same name.** `onError` on `renderToStream`/`renderToString` and on `handleServerFunctionRequest` _is_ the server error hook (`(error, context) => wire | void`); `onServerError` is removed. A one-argument `onError` written for the old shape keeps working and now hears every handled failure — filter on `context.handling === "failed"` for the request-failing ones alone. New `handling: "serialize"` for a hydration value that would not serialize (what seroval's `onError` reported, for a render that passed one). With no hook anywhere, a failure that fails the request still reaches `console.error`. + +- cc0396b: `_parent` joins `_name` as a field signals' property mangling reserves — the two cross-package owner fields. + + Signals' prod and observe artifacts rename every `_`-prefixed property except a reserved list; the dev artifact (which the test suites run against) is unmangled. Two things read `_parent` across the package boundary and only worked in dev: + - `solid-js`'s client hydration walks `owner._parent` to the root to mark the hydration snapshot scope. In the built prod and observe artifacts the walk found nothing and marked the current owner instead, so computations created outside that owner's subtree during hydration read live values rather than the server snapshot. + - The core's owner walks — `ownerPath` and `OBSERVE.exclude`/`isExcluded` — over `solid-js`'s server owners. `ownerPath` had a server-side shim (`located()`, now removed); `OBSERVE.exclude` was a silent no-op for a server owner outside dev. + + Cost: ~40 B brotli on the prod app scenarios; the observe scenarios did not grow. Pinned from both ends: `packages/solid/test/cross-package-fields.spec.ts` checks the reserved fields survive in the mangled artifacts and scans the built client artifacts of `solid-js`, `@solidjs/web` and `@solidjs/universal` for any signals `_` field that is not reserved; `packages/web/test/server/server-owner-walks.spec.tsx` runs `ownerPath` and `OBSERVE.exclude` over server owners against the built observe and development artifacts. + + Also: RFC 08 gains "Values in records — the PII surface", the complete list of record and finding fields that carry user data (value previews, interaction target text, navigation paths/params, `data.error` on the server error findings) for exporters that leave the process. + +- 53280e7: The error hooks and `SSR_RENDER_ERROR_CONTAINED` tell where an error was thrown apart from where it was met. + + `ownerPath` on `ClientErrorContext` and `ServerErrorContext` is now where the error was **thrown**: the labels root-first up the owner chain of the computation that threw — the component that broke — falling back to the boundary's chain when the throw crossed nothing the runtime could name. A new `boundaryPath` is where it was **met**: the same labels up the chain of the `` that rendered its fallback (client and server) or the `` that shipped the rejection (server, `handling: "client"`). Before, `ownerPath` was the boundary's on both sides, so every component under one boundary grouped into one path. On the client the engine's status wrapper already named the thrower (`StatusError.source`); on the server the owner scopes stamp it as the error escapes. The `SSR_RENDER_ERROR_CONTAINED` finding follows: `ownerPath` locates the throw, `data.boundary` / `data.boundaryPath` the boundary. + +- 3154ed6: A signal written from `onSettled` or `createEffect` during hydration no longer strands the DOM it reveals (#3504). Two halves: a root created while hydrating marks the hydration snapshot scope itself, so a write during the root pass is held until the pass completes and then replays — previously the scope was marked lazily by the first hydration-aware primitive, so a `` condition created before that sat outside it and the write cascaded mid-claim. And a streamed boundary's resume window now claims only the subtree under that boundary: a write from the resumed content that reaches a signal above it (a route's `onSettled` adding a toast to a provider) re-renders that already-hydrated region as a client render — fresh nodes, live inserts — instead of claiming against the server registry, missing with a "Hydration key miss" warning, and rendering detached. +- b298154: Move the runtimes' seams off the `solid-js` surface and behind `solid-js/internal` + + `merge()`/`omit()` returning lazy views (#3454) gave `spread()` and + `ssrElement()` a protocol for reading props leaf by leaf instead of trapping + through the proxy per key. Because `@solidjs/web` and `@solidjs/universal` + depend on `solid-js` alone — never on `@solidjs/signals` directly, so an app + holds exactly one reactive engine — every piece of that protocol went out + through `solid-js`'s main export: eleven names, `mergeSources` before them in + #3325, on the public surface with no marking. None of it is API. + + The protocol (`viewOf`, `mergeView`/`omitView`, `MergeView`/`OmitView`, + `sourceKeys`/`sourceHas`/`sourceGet`, `hasStaticKeys`, `resolvedTable`, the + `SOURCE_*` kinds, `SourceKind`) now lives on a `solid-js/internal` subpath, + along with the server-scope seams that were already `@internal` in JSDoc and + consumed only by `@solidjs/web` (`ssrHandleError`, `ssrScope`, + `runInServerComponentScope`, `inServerComponentScope`, `creationStamp`, + `getProjectionTrace`, `materializeContainerTrace`). The names stay exported + from the main entries at runtime, so the subpath shares one module state and + the single-engine guarantee is untouched; `stripInternal` keeps them out of + the generated declarations, so TypeScript no longer offers or types them. + `internal-surface.spec.ts` pins the boundary in both directions. + + Also dropped from the entries: `storeIsShallow`, `storeHasFamily`, + `storeHasOptimisticFamily` (leftovers of the gutted patch channel), + `storePath` and `$REFRESH` (referenced only by `@solidjs/signals`'s own + internals), and `NoHydrateContext` (`@internal`, used only by `solid-js`'s + server code). Nothing in the repo consumed them. + + No runtime behavior change. `merge`, `omit`, and the rest of the reactive + surface are unaffected. + +- 899c2c4: `merge()` and `omit()` are always lazy views, and props consumers read their leaves + + `omit(props, ...keys)` returns a live view of `props` for every input — a plain object included — instead of copying it with a `getOwnPropertyDescriptor` + `defineProperty` per prop. A predicate form hides keys by rule without enumerating first: `omit(props, k => k[0] === "$")`. `merge()` no longer builds an eager copy when its sources are plain objects: under `Proxy` it always returns an O(1) view over the flattened sources (a single non-function source is returned as is). + + The two compose flat. An `omit()` over a `merge()` carries one filtered view per flattened merge source, a `merge()` over an `omit()` takes the view record as a leaf, and nested omits fold their filters into one record. A component chain of `merge(defaults) → omit(consumed) → merge(statics) → omit("as")` — the shape headless-UI libraries render every element through — collapses to leaf views over the original objects, each with its accumulated filter, with no proxy layer left between the outermost spread and the author's props. `merge()` keeps the omitted keys hidden by construction (#3014) rather than by treating the omit as opaque. Construction cost drops 3–7× at depth 1–7; the SSR polymorphic-chain bench (#3448) runs ~2.4× faster. + + Reads stay cheap: a view over plain objects resolves a key → owning-leaf table once, on first read, and every `get`/`has`/descriptor is one lookup after that. `spread()` (DOM and universal) and `ssrElement()` read the leaves directly — never through the proxies' traps — and walk that table when there is one, so an effect rerun costs one read per key, as it did over the copy. Both proxies use a class target and one shared handler (no per-instance closures). + + A view over a store asks the store nothing but the read. Each source's kind (plain object, omit record, proxy, memo) is decided once, when the view is built, and carried beside it — every brand check on a Proxy is a trap (`instanceof` is a `getPrototypeOf` trap, as expensive as a store read), and store detection goes through `$TARGET`, a symbol the store's `get` trap answers on its fast path, never its generic tracked-read path. `merge(defaults, store)` constructs ~30% faster than the copy did and reads ~15% faster; `omit(store)` reads at parity. + + The views tell the truth: `Object.getOwnPropertyDescriptor(view, key)` reports a data descriptor only when the key is a data property of a plain leaf (the compiler's encoding of a static prop) and an accessor for a getter, a store key, or a memo source. Together with the new internal `hasStaticKeys()`, `spread()` now skips the children effect for static children behind `omit`/`merge` layers (#3388 through views). + + Behavior changes: + - Writes to a `merge()` or `omit()` result are no-ops (they already were for the proxy forms). A caller that needs its own object copies it (`{ ...merged }`), and the copy carries no sources (#3384). `@solidjs/html` now collects its own props and spreads into one `merge()` at the end instead of assigning onto the result. + - A data property on a source is read live through the view rather than snapshotted at `merge()`/`omit()` time. + - Key order of a merged view is the merged order — every key at the position of the last source that carries it — matching `ssrElement`'s array form. + - Sources are treated as own-keyed; a key added to a plain source after merging is not seen (the copy did not see it either). + - Enumerating a view through its traps (`for…in`, `Object.keys`, `{ ...view }`) costs a trap per key, as any proxy does; the internal consumers avoid it. Environments without `Proxy` keep the copy paths. + + Internal helpers for consumers, exported from `solid-js`: `viewOf(o)`, `mergeView(o)`, `omitView(o)`, `sourceKeys(entry, kind)`, `sourceHas(entry, kind, key)`, `sourceGet(entry, kind, key)`, `hasStaticKeys(o)`, `resolvedTable(o)`, the `SOURCE_*` kinds. + +- 3ae9e92: `OBSERVE.records` — one records channel on both platforms (observe/dev tiers); frame records from both ends; the client `"call"` record; `observeServerFunctionCalls` removed + - **`@solidjs/signals`**: `OBSERVE.records` — `subscribe(type, listener)`, `observed(type)`, `emit(type, event, live)` — the channel every runtime record rides, created once per process and registered on `globalThis` under `Symbol.for("@solidjs/signals/observe/records")` so a second copy of the core (a bundled server build instrumented through `--import`) and wire layers bundled without a framework import reach the same listener sets. Listeners are snapshotted per emit; a throwing listener is reported and the rest run. Types: `Records`, `RecordTypes` (extends `HostRecordTypes`; both declared empty, for the runtimes to augment — one augmenter per interface), `RecordType`, `RecordEvent`, `RecordLive`, `RecordListener`. Folds out of prod. New **`OBSERVE.attribution.currentOrigin()`** (and the `currentOrigin` hook on `AttributionHooks`): the provenance a root write performed now would be stamped with — the interaction whose handler is running, the navigation/effect/action frame open, or inside a recompute the origin of the change that caused it — as the engine's own `ChangeOrigin` object, `undefined` when external or with no engine; for a runtime stamping a record of its own. The installed hooks are also registered on `globalThis` under `Symbol.for("@solidjs/signals/observe/attribution")`, the same reach-without-an-import the channel has. + - **`solid-js`**: the `"boundary"` record moves from `OBSERVE.server.records` to `OBSERVE.records` (augmenting the core's `RecordTypes`). `OBSERVE.server` keeps only the `trace` slot; `ServerRecords` is gone. + - **`@solidjs/web`**: the `"invocation"` and `"frame"` records move to `OBSERVE.records` (augmenting `HostRecordTypes` through `solid-js`). New **`"call"` record** (`CallEvent`, `CallLive`, `CallListener`): one per server-function call made from the browser, at the caller's settle — `{ id, at, durationMs, method: "GET" | "POST", outcome, status?, origin?, deferred? }` with `{ args, response?, result? | error? }` beside it; joins the server's `"invocation"` by `id`, and — through `origin`, the engine's own interaction/navigation object read at dispatch via `currentOrigin()` — the attribution engine's `InteractionEvent` / `NavigationEvent` / `HoldEvent` by identity, so an observer files the call under the click that made it without a time join. The **`"frame"` record now has a client half**: `FrameEvent` is `FrameProducedEvent | FrameAppliedEvent`, discriminated by `side`, same census on both; the client half (`applyFrameResponse`, one per stream in a response) adds `address` (the `as` remap) and `outcome: "truncated"` for a body that ended before `complete`, with `live.response`. Server census fix: `regions` counts `html` chunks addressed to a child frame id (the former count read a chunk type that does not exist), and `shellMs` is set by the stream's own shell only. The emitters and their wrappers fold out of the prod client artifacts behind the observe literal (prod `applyFrameResponse` and the server-function dispatch are the pre-existing functions, no extra frame or promise hop). The server-functions and frames **client** entries gain `observe` and `development` builds and export conditions (`server-functions/dist/client.{observe,dev}.js`, `frames/dist/client.observe.js`); the server-functions client is now built with its flags replaced in every tier (before, `_SOLID_DEV_` there was an unreplaced truthy string). + - **Removed**: `observeServerFunctionCalls` and the `ServerFunctionCall` / `ServerFunctionRequestCall` / `ServerFunctionResponseCall` types, from both server-function entries. Subscribe to `OBSERVE.records` `"call"` (client) or `"invocation"` (server) instead. + - **`@solidjs/diagnostics`** (format v6): `artifact.server` is replaced by `artifact.records: { boundary, invocation, frame, call }` — always present, captured on both platforms including the browser bridge; types `BoundaryRecord`, `InvocationRecord`, `FrameRecord` (`FrameProducedRecord | FrameAppliedRecord`), `CallRecord` (with `origin?: ChangeOrigin`), `ArtifactRecords` replace the `Server*Record` / `ArtifactServer` names. JSONL: one line per record with `type` naming its table; the meta line's `boundaryCount`/`invocationCount`/`frameCount` become `recordCounts: { boundary, invocation, frame, call }`. + +- 328580f: `omit()` over a `merge()` is one record that holds the merge's record, not one leaf view per merge source + + An omit over a merge used to flatten at construction: one `OmitView` plus one combined hidden-key list per flattened leaf, and the next `merge()` copied those entries into its own arrays. On a component chain of defaults + omit + spread (Kobalte-shaped: `merge(omit(merge(omit(props))))`, four layers) that was ~19 records and as many list copies per element — the largest allocation of the render. The omit now holds the `MergeView` record itself (a new source kind, `SOURCE_MERGE`) and is one record however many leaves the merge has; a later `merge()` carries it as one entry, and a later `omit()` folds into it. Nothing is read through a proxy trap along the way: the entry helpers (`sourceKeys`/`sourceHas`/`sourceGet`, `hasStaticKeys`, descriptors, the resolved table) recurse into the record by function call. + - Reads through the nesting are one walk per read (a nested entry answers presence and value together), and a nested record counts no reads of its own toward the table threshold — the view that was asked decides for the whole tree, and its table is collected in one pass over the leaves rather than one table per layer. + - New `@internal` `sourceOwners(source, keys, owners)`: every key of a props source — a plain object, a store, or a merge/omit view — appended in merged order with the object that owns it, in one pass, later sources moving a key to the end. `ssrElement` collects any spread that is not plain objects only (a view, a store, the array form with one among them) this way, so each attribute is one direct read of its owner — no `in` walk per key through the layers, no key list per leaf, no table, and no per-entry classification (`pushEntry` is gone). + - An omit's `$SOURCES` never answers anything now (previously its filtered leaf views); consumers reach the record through `viewOf` and walk it as one filtered entry. + + Measured against `next` (interleaved, min of N, quiet machine): the tier-1 polymorphic-chain SSR harness allocates 12% less per row (14.6 → 12.8 KB) and is 2–6% faster across the interpreter, Sparkplug, Maglev and TurboFan tiers; the props-chain microbench builds 6–65% faster and builds+consumes 7–31% faster by depth and tier; the omit/merge micro-suite is flat or better in every shape. On the yak-bench SSR lanes with every yak piece on Solid primitives: +7% geomean, +20–36% on the component-composition cases (`polymorphic-chain`, `tabs`, `multifile-composition`), which brings those to parity with yak's hand-rolled runtime. + +- 61a114c: Server boundary records on `OBSERVE.server.records` (observe/dev tiers) + - New `"boundary"` record: one per `` boundary that waited during a server render, delivered when it settles — `{ id, at, durationMs, heldMs, passes, outcome: "settled" | "fallback" | "client" | "error", streamed, revealGroup?, ownerPath? }`, with the thrown error beside it. `id` pairs it with `SSR_RENDER_ERROR_CONTAINED`; `passes` counts render passes (a sequential chain reads as `3+`); under a `` group the record waits for the group's swap so `heldMs` measures how long finished content was held for its siblings. No clock is read without a listener, and the emitter folds out of prod. + - The server records channel is `OBSERVE.server.records.subscribe(type, listener)` — the server twin of `OBSERVE.attribution.subscribe(type, …)`. `OBSERVE.server.invocations` (unreleased) is renamed onto it: `subscribe("invocation", …)`. The `InvocationChannel` type is gone; `ServerRecords` is the channel's interface. + - Types now layer one augmenter per interface: `@solidjs/signals` declares `ServerObserve` empty; `solid-js` augments it with `records: ServerRecords` and `trace: ServerTrace`, declaring both; `@solidjs/web` augments those two through `"solid-js"`. `TraceSlot` in `@solidjs/web` is now an alias of `solid-js`'s `ServerTrace`. (Two augmentations of one re-exported interface through different module aliases merge order-dependently in TypeScript — one set was silently lost.) + - `@solidjs/signals` exports `ownerPath(subject)` — the root-first component-label walk its diagnostics already make — so a record's `ownerPath` and the finding it pairs with come from the one walk. + +- 0d8347a: Server records reach the diagnostics artifact and the dev checks (server-dev-build-plan P4) + - `@solidjs/diagnostics` artifact format **v5**: `artifact.server: { boundaries, invocations } | null` folds `OBSERVE.server.records` when the scenario runs under the server runtime — `captureArtifact(() => renderToStream(…))` — one row per `` boundary that waited and per server-function execution; `null` for client captures and the browser bridge. New exported types `ArtifactServer`, `ServerBoundaryRecord`, `ServerInvocationRecord` (mirrors of the runtime's `BoundaryEvent`/`InvocationEvent`; the package still depends on `@solidjs/signals` alone). JSONL egress adds `boundary` and `invocation` lines and the header counts. + - `InvocationEvent.boundary`: a direct server-function call made during a `` boundary's render pass carries that boundary's hydration id, the `"boundary"` record's `id` — the join between a boundary's wait and the calls under it. + - Two dev checks derived from the boundary facts in `ssrLoadingBoundary`: `ASYNC_WATERFALL` with `data.side: "server"` (`passes - 1` sequential flights; 2 → `info`, structured only; 3+ → console `warn`) and a new code `SSR_CLIENT_CONTENT_MASKED` (`warn`, `ssr`) for client-only content that surfaced only after a real server wait — the server's work discarded, the fallback shown for the wait. Dev tier only; the boundary clock now runs in dev without a listener. + - `solid-js`'s server `emitFinding` keeps `info` findings off the console (structured channel only), matching the core. + +- 7623ce1: Server diagnostics on `OBSERVE.diagnostics`; `OBSERVE.server` owned by `solid-js`'s server entry + + The server runtime now reports on the same structured channel as the client. **Findings** — facts about a render, present in observe and dev builds — `SSR_RENDER_ERROR_CONTAINED` (a render error a boundary routed; `data.handling` is `fallback`, `client`, or `failed` — the structured face of what `renderToStream`'s `onError` receives, on the process-wide channel), `SSR_SUBTREE_ABANDONED` (a failed fragment's pending descendants discarded), `SSR_STREAM_ABANDONED` (consumer cancelled or sink failed mid-render), `LATE_HEADER_WRITE` (recorded beside the existing dev throw / prod log), `SERVER_FN_ERROR_SANITIZED` (the original error the production wire replaced), and `FRAME_MARKER_CORRUPTED` from the frames client. **Checks** — dev-only guidance — convert every server `console.warn` to a code: `SERVER_WRITE`, `REVEAL_IN_RENDER_TO_STRING`, `LAZY_ASSET_UNMAPPED`, `PRELOAD_DESCRIPTOR_INVALID`, `HEAD_TAG_INVALID`, `BEHAVIOR_CLAIM_DROPPED`, and `UNRECOGNIZED_INSERT_VALUE` (now one code and a `render` kind on both platforms); `ASYNC_OUTSIDE_LOADING_BOUNDARY` on the server records with `data.side: "server"` before it throws. Server components are labelled for `ownerPath` (`createComponent` runs the body under a transparent `` owner in observe/dev — no hydration id consumed), so `in ` reads the same on both sides, and the server entry installs the same repair-guide console footer as the client. Prod artifacts carry none of it; `DiagnosticKind` gains `ssr`, `head`, `render`. + + `OBSERVE.server`'s objects (the invocation listener set, the trace-provider slot) are now created by `solid-js`'s server entry, once per process under `Symbol.for("solid-js/observe/server")` on `globalThis`, rather than by `@solidjs/web`'s module init: an observer's `init()` that imports only `solid-js` can subscribe and `provide` before the web runtime has loaded, and a host that bundles the runtime into its server build and instruments through a `--import`ed module finds one listener set and one provider across both copies. The core keeps `server: {}`; the client pays nothing. + +- 56858e7: The server error hook — `configureServerErrors({ onError })` from `@solidjs/web`, `onServerError` on `renderToStream`/`renderToString` and on `handleServerFunctionRequest`: the prod-tier seam through which a server integration hears every failure the runtime handles and may say what the client receives instead (sentry-integration-plan C6, #3468 part 3). + + Until now a production build reported only the failure that fails a request (`renderToStream`'s `onError`); an `` fallback rendered, a `` fragment rejected and re-rendered by the client, and a server-function throw sanitized onto the wire were observe-tier findings only. The hook is called **once per error object, at first sight**, with where the failure was met — `kind: "render"` (`fallback` / `client` / `failed`, with the boundary's hydration id and the component labels when compiled in) or `kind: "server-function"` (`thrown` / `channel`, with `functionId` and `direct` for an in-process call during SSR) — and the request event. Its return is the **wire value**: rendered into the fallback, serialized for hydration, sent as the RPC error; `undefined` keeps the default policy (generic outside dev, fidelity in dev); ignored for `failed`. A direct server-function call that throws during SSR is reported once as the function's failure, and the boundary that contains it reuses the verdict. Two tiers as `wrapInvocation` has: ambient (registered on `globalThis` under `Symbol.for("solid-js/server/errors")`, shared by a bundled build and an `--import`ed module) and per request, the latter winning. A throwing hook is reported and treated as silent. The hook fires in every tier; `onError` keeps its meaning. + + Internals: `solid-js`'s server entry owns the verdict engine (`reportServerError` / `ssrSanitizeError` through `solid-js/internal`, one cache per error object); the SSR context gains `errorPolicy` and `flushed`; the `_fr` rejection reads its verdict at delivery. A rejected async source's own serialized rejection is encoded in the rejection's microtask, ahead of the boundary, and carries the default policy's value — the mapping reaches the boundary's record, which the hydrating client renders from; no tick is added to the error path. `SSR_ERROR_SANITIZED` carries `data.wire`. + +- af94f67: Server observe surface: `OBSERVE.server` and the invocation channel + + `OBSERVE` gains a `server` slot — an augmentable `ServerObserve` interface declared empty in `@solidjs/signals` (re-exported by `solid-js`), typed and emitted into by `@solidjs/web`'s server runtime, so server-side observability consumers subscribe on the one `OBSERVE` object they already know from the client. The first channel is `OBSERVE.server.invocations`: `subscribe("invocation", (event, live) => …)` delivers one `{ id, direct, at, durationMs, outcome, deferred? }` record per server-function execution — HTTP dispatch and direct SSR calls alike — when it settles, with the request event, `request`, `args`, and the result or the error as thrown beside it. Observers, not policy: any number of listeners, none able to alter the call; `wrapInvocation` remains the single policy hook. + + `@solidjs/web` now publishes observe-tier server artifacts (`dist/server.observe.js`, `server-functions/dist/server.observe.js`, `frames/dist/server.observe.js`) under the `observe` export condition, alongside the existing dev/prod pairs. The surface and every emit site fold out of the prod artifacts. + +- e87d694: `{ shallow: true }` computed stores keep their leaves raw on every path (#3498). The projection draft no longer wraps a nested value in a draft proxy — leaf identity holds and a frozen leaf can no longer trip a Proxy invariant — and the loading shadow, its commit copy, the SSR draft and snapshots, and the hydration replay shadow copy only the root container instead of JSON-cloning the tree, which turned `Date` into a string, `NaN` into `null`, dropped `undefined` properties, and could not represent BigInt or cycles. Deep stores are unchanged. +- 5426ffb: Fix a server-rendered `` fallback hydrating dead (#3414). When the boundary's children threw synchronously on the server and the fallback was a zero-arg thunk (`fallback={() => }`), the thunk was handed back unresolved and unwrapped by the enclosing boundary — the client does that inside the boundary's flatten computed, the server did it inline under the boundary owner — so the fallback's hydration keys disagreed, its root element failed to claim the server node, and its event handlers and effects never attached. The server `Errored` and `Loading` boundaries now resolve their children's result in a virtual id scope mirroring the client's second computed; the error record stays at the boundary id. +- 63560a1: `componentNames` now applies to SSR output. Under the option both compilers keep the `createComponent` call they otherwise inline to `Comp(props)` and pass the source tag name — `createComponent(Comp, props, "Comp")` — so the server runtime's observe/dev `createComponent` labels the owner and a server finding's `ownerPath` reads `` like the client's. Without the option (prod builds) SSR output is unchanged. `@solidjs/vite-plugin` already passes the option for its dev and observe postures, so app server builds pick this up with no config change. + + Fixes `ssrScope` under transparent owners: the virtual hole scope swapped the current owner's id counter, but content inside a hole resolves ids by walking past transparent owners, so with one in between (the server-component scope owner; now the labelled component owner) the hole's content took ids from the enclosing counter and disagreed with the client. The scope now swaps the nearest id-bearing owner. + +- 34287d8: SSR render failures reach the client sanitized (#3468): the wire policy the server-function handler has applied since #3113/#3116 now covers every SSR road a failure takes — the error an `` serializes for hydration, a rejected async source serialized into the stream, a `` fragment's `_fr` rejection, a frame stream's error chunks (the fragment's, a live hole's, the root's). Outside the dev build a plain thrown value is replaced with a generic `Error` (`"Internal Server Error"`); `message`, `cause` and own properties stay on the server. A `"use server"` function called in-process during SSR never touches the RPC wire, so before this the production page load shipped what that wire withholds. + - **`solid-js`** (server): `` sanitizes _before_ rendering its fallback and serializes the same replacement, so fallback markup and the hydration record agree. A fallback printing `err().message` shows the generic message in production, as for a server-function failure. `markSafeError` (`Symbol.for("solid.SafeError")`) passes through with own properties; an Error reached as a _value_ is data and passes as written (#3113's ruling). One replacement per original, however many roads it takes. New finding `SSR_ERROR_SANITIZED` (`info`, observe + dev; `data.error` the original), beside `SSR_RENDER_ERROR_CONTAINED` which carries the failure itself. `ssrSanitizeError` is exposed to the runtimes through `solid-js/internal`. Server findings now carry their `ownerPath` in the **observe** artifact too — the core's walk reads `_parent` under its own build's property mangling and found nothing on a server owner there, so the server entry locates its findings itself. + - **`@solidjs/web`** (server): the hydration serialize funnel guards every channel it writes (a promise's rejection, an async iterable's thrown step); a fragment's terminal error reaches the `_fr` rejection and a transport sink's error chunk sanitized while the abandonment ledger keeps the original; the frame sink's root error chunk and the live-hole/live-attribute error chunks carry the replacement's message. + - **`@solidjs/signals`**: the `SSR_ERROR_SANITIZED` code. + + The dev/prod line is the build variant: the `development` server artifacts keep full fidelity; production and observe sanitize. + +- Updated dependencies [8ff4803] +- Updated dependencies [e9c464b] +- Updated dependencies [0da94f9] +- Updated dependencies [8bf04ea] +- Updated dependencies [eaa7e33] +- Updated dependencies [ebedb44] +- Updated dependencies [a8a8949] +- Updated dependencies [d80cd1f] +- Updated dependencies [d826cd3] +- Updated dependencies [cc0396b] +- Updated dependencies [d2a36f5] +- Updated dependencies [50323b4] +- Updated dependencies [53280e7] +- Updated dependencies [d7cb456] +- Updated dependencies [a0d6dd2] +- Updated dependencies [c3ae310] +- Updated dependencies [6095955] +- Updated dependencies [e80f241] +- Updated dependencies [84adf0b] +- Updated dependencies [5c1f01f] +- Updated dependencies [a5d8eae] +- Updated dependencies [14ded24] +- Updated dependencies [25c5064] +- Updated dependencies [1ce0f85] +- Updated dependencies [05c7e21] +- Updated dependencies [9da7f0a] +- Updated dependencies [62b0a22] +- Updated dependencies [27b24aa] +- Updated dependencies [549f482] +- Updated dependencies [d80cd1f] +- Updated dependencies [76230f9] +- Updated dependencies [347a5ca] +- Updated dependencies [a8a8949] +- Updated dependencies [632e45c] +- Updated dependencies [75c5113] +- Updated dependencies [899c2c4] +- Updated dependencies [ca05917] +- Updated dependencies [3ae9e92] +- Updated dependencies [328580f] +- Updated dependencies [0bffee2] +- Updated dependencies [f329a26] +- Updated dependencies [c827758] +- Updated dependencies [6e9243c] +- Updated dependencies [7a09cd9] +- Updated dependencies [61a114c] +- Updated dependencies [0d8347a] +- Updated dependencies [7623ce1] +- Updated dependencies [af94f67] +- Updated dependencies [e87d694] +- Updated dependencies [dd19e9e] +- Updated dependencies [c245532] +- Updated dependencies [34287d8] +- Updated dependencies [64f9266] +- Updated dependencies [0148d58] +- Updated dependencies [765a656] +- Updated dependencies [9db33cf] +- Updated dependencies [bfd6f6c] +- Updated dependencies [2054045] +- Updated dependencies [c410709] +- Updated dependencies [f555ec2] +- Updated dependencies [d8e35a3] +- Updated dependencies [5f7da9d] +- Updated dependencies [7f5f902] +- Updated dependencies [31adfce] + - @solidjs/signals@2.0.0-rc.9 + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/solid/package.json b/packages/solid/package.json index 701f96bd8..131cdef16 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,7 +1,7 @@ { "name": "solid-js", "description": "Reactive JavaScript library for building user interfaces. Compiles JSX to real DOM with fine-grained signal-based updates — no virtual DOM.", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "author": "Ryan Carniato", "license": "MIT", "homepage": "https://solidjs.com", @@ -127,7 +127,7 @@ "performance" ], "dependencies": { - "@solidjs/signals": "^2.0.0-rc.8", + "@solidjs/signals": "^2.0.0-rc.9", "csstype": "^3.1.0", "seroval": "~1.6.7", "seroval-plugins": "~1.6.7" diff --git a/packages/test-integration/CHANGELOG.md b/packages/test-integration/CHANGELOG.md index a8605ba3c..f900f960d 100644 --- a/packages/test-integration/CHANGELOG.md +++ b/packages/test-integration/CHANGELOG.md @@ -1,5 +1,107 @@ # test-integration +## 2.0.0-rc.9 + +### Patch Changes + +- Updated dependencies [8ff4803] +- Updated dependencies [e9c464b] +- Updated dependencies [0da94f9] +- Updated dependencies [8bf04ea] +- Updated dependencies [eaa7e33] +- Updated dependencies [ebedb44] +- Updated dependencies [a8a8949] +- Updated dependencies [d80cd1f] +- Updated dependencies [d826cd3] +- Updated dependencies [8d6de07] +- Updated dependencies [cc0396b] +- Updated dependencies [6d2bdeb] +- Updated dependencies [17b0bda] +- Updated dependencies [1af28a1] +- Updated dependencies [d2a36f5] +- Updated dependencies [c452850] +- Updated dependencies [50323b4] +- Updated dependencies [53280e7] +- Updated dependencies [d7cb456] +- Updated dependencies [a0d6dd2] +- Updated dependencies [c3ae310] +- Updated dependencies [6095955] +- Updated dependencies [e80f241] +- Updated dependencies [84adf0b] +- Updated dependencies [5c1f01f] +- Updated dependencies [a5d8eae] +- Updated dependencies [14ded24] +- Updated dependencies [25c5064] +- Updated dependencies [1ce0f85] +- Updated dependencies [05c7e21] +- Updated dependencies [9da7f0a] +- Updated dependencies [5b31076] +- Updated dependencies [084e621] +- Updated dependencies [a429b44] +- Updated dependencies [62b0a22] +- Updated dependencies [246eeeb] +- Updated dependencies [27b24aa] +- Updated dependencies [549f482] +- Updated dependencies [3154ed6] +- Updated dependencies [b298154] +- Updated dependencies [d80cd1f] +- Updated dependencies [76230f9] +- Updated dependencies [347a5ca] +- Updated dependencies [a8a8949] +- Updated dependencies [632e45c] +- Updated dependencies [75c5113] +- Updated dependencies [899c2c4] +- Updated dependencies [ca05917] +- Updated dependencies [3ae9e92] +- Updated dependencies [328580f] +- Updated dependencies [0bffee2] +- Updated dependencies [f329a26] +- Updated dependencies [da6ed76] +- Updated dependencies [5f688a6] +- Updated dependencies [c827758] +- Updated dependencies [6e9243c] +- Updated dependencies [c66130d] +- Updated dependencies [36db287] +- Updated dependencies [7a09cd9] +- Updated dependencies [61a114c] +- Updated dependencies [0d8347a] +- Updated dependencies [7623ce1] +- Updated dependencies [56858e7] +- Updated dependencies [537faea] +- Updated dependencies [af94f67] +- Updated dependencies [042b540] +- Updated dependencies [e87d694] +- Updated dependencies [dd19e9e] +- Updated dependencies [c245532] +- Updated dependencies [7f6332a] +- Updated dependencies [5426ffb] +- Updated dependencies [63560a1] +- Updated dependencies [a732d2b] +- Updated dependencies [40977c9] +- Updated dependencies [34287d8] +- Updated dependencies [64f9266] +- Updated dependencies [0148d58] +- Updated dependencies [765a656] +- Updated dependencies [9db33cf] +- Updated dependencies [bfd6f6c] +- Updated dependencies [2054045] +- Updated dependencies [c410709] +- Updated dependencies [f555ec2] +- Updated dependencies [d8e35a3] +- Updated dependencies [48f007e] +- Updated dependencies [5f7da9d] +- Updated dependencies [350f65f] +- Updated dependencies [1643d2a] +- Updated dependencies [7f5f902] +- Updated dependencies [31adfce] + - @solidjs/signals@2.0.0-rc.9 + - solid-js@2.0.0-rc.9 + - @solidjs/web@2.0.0-rc.9 + - @solidjs/babel-plugin@2.0.0-rc.9 + - @solidjs/h@2.0.0-rc.9 + - @solidjs/universal@2.0.0-rc.9 + - @solidjs/html@2.0.0-rc.9 + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/test-integration/package.json b/packages/test-integration/package.json index 6b07ae503..323b41a93 100644 --- a/packages/test-integration/package.json +++ b/packages/test-integration/package.json @@ -21,5 +21,5 @@ "gitly": "^2.2.1", "shelljs": "^0.8.5" }, - "version": "2.0.0-rc.8" + "version": "2.0.0-rc.9" } diff --git a/packages/universal/CHANGELOG.md b/packages/universal/CHANGELOG.md index d4c8d3f86..96174fa24 100644 --- a/packages/universal/CHANGELOG.md +++ b/packages/universal/CHANGELOG.md @@ -1,5 +1,82 @@ # @solidjs/universal +## 2.0.0-rc.9 + +### Patch Changes + +- b298154: Move the runtimes' seams off the `solid-js` surface and behind `solid-js/internal` + + `merge()`/`omit()` returning lazy views (#3454) gave `spread()` and + `ssrElement()` a protocol for reading props leaf by leaf instead of trapping + through the proxy per key. Because `@solidjs/web` and `@solidjs/universal` + depend on `solid-js` alone — never on `@solidjs/signals` directly, so an app + holds exactly one reactive engine — every piece of that protocol went out + through `solid-js`'s main export: eleven names, `mergeSources` before them in + #3325, on the public surface with no marking. None of it is API. + + The protocol (`viewOf`, `mergeView`/`omitView`, `MergeView`/`OmitView`, + `sourceKeys`/`sourceHas`/`sourceGet`, `hasStaticKeys`, `resolvedTable`, the + `SOURCE_*` kinds, `SourceKind`) now lives on a `solid-js/internal` subpath, + along with the server-scope seams that were already `@internal` in JSDoc and + consumed only by `@solidjs/web` (`ssrHandleError`, `ssrScope`, + `runInServerComponentScope`, `inServerComponentScope`, `creationStamp`, + `getProjectionTrace`, `materializeContainerTrace`). The names stay exported + from the main entries at runtime, so the subpath shares one module state and + the single-engine guarantee is untouched; `stripInternal` keeps them out of + the generated declarations, so TypeScript no longer offers or types them. + `internal-surface.spec.ts` pins the boundary in both directions. + + Also dropped from the entries: `storeIsShallow`, `storeHasFamily`, + `storeHasOptimisticFamily` (leftovers of the gutted patch channel), + `storePath` and `$REFRESH` (referenced only by `@solidjs/signals`'s own + internals), and `NoHydrateContext` (`@internal`, used only by `solid-js`'s + server code). Nothing in the repo consumed them. + + No runtime behavior change. `merge`, `omit`, and the rest of the reactive + surface are unaffected. + +- 899c2c4: `merge()` and `omit()` are always lazy views, and props consumers read their leaves + + `omit(props, ...keys)` returns a live view of `props` for every input — a plain object included — instead of copying it with a `getOwnPropertyDescriptor` + `defineProperty` per prop. A predicate form hides keys by rule without enumerating first: `omit(props, k => k[0] === "$")`. `merge()` no longer builds an eager copy when its sources are plain objects: under `Proxy` it always returns an O(1) view over the flattened sources (a single non-function source is returned as is). + + The two compose flat. An `omit()` over a `merge()` carries one filtered view per flattened merge source, a `merge()` over an `omit()` takes the view record as a leaf, and nested omits fold their filters into one record. A component chain of `merge(defaults) → omit(consumed) → merge(statics) → omit("as")` — the shape headless-UI libraries render every element through — collapses to leaf views over the original objects, each with its accumulated filter, with no proxy layer left between the outermost spread and the author's props. `merge()` keeps the omitted keys hidden by construction (#3014) rather than by treating the omit as opaque. Construction cost drops 3–7× at depth 1–7; the SSR polymorphic-chain bench (#3448) runs ~2.4× faster. + + Reads stay cheap: a view over plain objects resolves a key → owning-leaf table once, on first read, and every `get`/`has`/descriptor is one lookup after that. `spread()` (DOM and universal) and `ssrElement()` read the leaves directly — never through the proxies' traps — and walk that table when there is one, so an effect rerun costs one read per key, as it did over the copy. Both proxies use a class target and one shared handler (no per-instance closures). + + A view over a store asks the store nothing but the read. Each source's kind (plain object, omit record, proxy, memo) is decided once, when the view is built, and carried beside it — every brand check on a Proxy is a trap (`instanceof` is a `getPrototypeOf` trap, as expensive as a store read), and store detection goes through `$TARGET`, a symbol the store's `get` trap answers on its fast path, never its generic tracked-read path. `merge(defaults, store)` constructs ~30% faster than the copy did and reads ~15% faster; `omit(store)` reads at parity. + + The views tell the truth: `Object.getOwnPropertyDescriptor(view, key)` reports a data descriptor only when the key is a data property of a plain leaf (the compiler's encoding of a static prop) and an accessor for a getter, a store key, or a memo source. Together with the new internal `hasStaticKeys()`, `spread()` now skips the children effect for static children behind `omit`/`merge` layers (#3388 through views). + + Behavior changes: + - Writes to a `merge()` or `omit()` result are no-ops (they already were for the proxy forms). A caller that needs its own object copies it (`{ ...merged }`), and the copy carries no sources (#3384). `@solidjs/html` now collects its own props and spreads into one `merge()` at the end instead of assigning onto the result. + - A data property on a source is read live through the view rather than snapshotted at `merge()`/`omit()` time. + - Key order of a merged view is the merged order — every key at the position of the last source that carries it — matching `ssrElement`'s array form. + - Sources are treated as own-keyed; a key added to a plain source after merging is not seen (the copy did not see it either). + - Enumerating a view through its traps (`for…in`, `Object.keys`, `{ ...view }`) costs a trap per key, as any proxy does; the internal consumers avoid it. Environments without `Proxy` keep the copy paths. + + Internal helpers for consumers, exported from `solid-js`: `viewOf(o)`, `mergeView(o)`, `omitView(o)`, `sourceKeys(entry, kind)`, `sourceHas(entry, kind, key)`, `sourceGet(entry, kind, key)`, `hasStaticKeys(o)`, `resolvedTable(o)`, the `SOURCE_*` kinds. + +- 1643d2a: The universal renderer's `spread()` follows the `@solidjs/web` contract (#3388): `ref` folds into the props effect and is re-applied only when its identity changes (refs run with no owner, so nothing they create is disposed by the fold); children keep their own owned `insert` — that effect owns the child subtree — but a plain object whose `children` is a data property inserts the value with no effect at all. Three reactive nodes become two when children flow through the spread, one when they don't. `spread` also resolves a lone function source inside its own tracking scopes and accepts an array of sources — `spread(node, [a, b], skipChildren)` — the union of their keys with later sources winning, only the winning source read, function sources called inline with no merge and no memo. Both compilers' universal output uses it: a lone spread passes straight through (reactive included, no more `mergeProps(() => …)`), and several sources compile to the array instead of a `mergeProps()` call. +- Updated dependencies [8bf04ea] +- Updated dependencies [d826cd3] +- Updated dependencies [cc0396b] +- Updated dependencies [53280e7] +- Updated dependencies [3154ed6] +- Updated dependencies [b298154] +- Updated dependencies [899c2c4] +- Updated dependencies [3ae9e92] +- Updated dependencies [328580f] +- Updated dependencies [61a114c] +- Updated dependencies [0d8347a] +- Updated dependencies [7623ce1] +- Updated dependencies [56858e7] +- Updated dependencies [af94f67] +- Updated dependencies [e87d694] +- Updated dependencies [5426ffb] +- Updated dependencies [63560a1] +- Updated dependencies [34287d8] + - solid-js@2.0.0-rc.9 + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/universal/package.json b/packages/universal/package.json index 025361629..baf3b2df3 100644 --- a/packages/universal/package.json +++ b/packages/universal/package.json @@ -1,7 +1,7 @@ { "name": "@solidjs/universal", "description": "Solid's universal runtime for creating custom renderers", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "author": "Ryan Carniato", "license": "MIT", "homepage": "https://solidjs.com", @@ -51,7 +51,7 @@ "test": "vitest run" }, "peerDependencies": { - "solid-js": "^2.0.0-rc.8" + "solid-js": "^2.0.0-rc.9" }, "devDependencies": { "solid-js": "workspace:*" diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index 43f91cff8..ffe99347d 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -1,5 +1,251 @@ # @solidjs/web +## 2.0.0-rc.9 + +### Patch Changes + +- d826cd3: The client error hook — `configureClientErrors({ onError })` from `solid-js`, and `render`/`hydrate`'s `onError` option in `@solidjs/web`: the prod-tier seam through which an app, or an error monitor, hears the one failure nothing else can see — an `` / `createErrorBoundary` collected it and renders its fallback. The twin of the server's `configureServerErrors`. An uncaught error is not this hook's: the halt (`REACTIVITY_HALTED`) hands its cause to the platform's `reportError`, the channel every monitor already listens on. + + Once per error object (a `reset()` re-collecting the same failure says nothing new; a primitive is reported per sight); `ownerPath` carries the component labels where the runtime keeps owner names; no return — the client has no wire to map for; a throwing hook is reported and ignored. A root's own hook wins over the ambient one for failures under it. + + Pay-for-use: the hook machinery (`core/error-hooks.ts`) is retained by `createErrorBoundary` or the app's own `configureClientErrors` import; a root's hook is parked on the root owner under the registered `ROOT_ERROR_HOOK` symbol (defined in the scheduler), so `render` retains nothing for an app that passes none. Core floor unchanged; apps with a boundary +~200 B. + + **The server surface consolidates on the same name.** `onError` on `renderToStream`/`renderToString` and on `handleServerFunctionRequest` _is_ the server error hook (`(error, context) => wire | void`); `onServerError` is removed. A one-argument `onError` written for the old shape keeps working and now hears every handled failure — filter on `context.handling === "failed"` for the request-failing ones alone. New `handling: "serialize"` for a hydration value that would not serialize (what seroval's `onError` reported, for a render that passed one). With no hook anywhere, a failure that fails the request still reaches `console.error`. + +- 6d2bdeb: Move delegated event handlers off the `$$` element key Solid 1 uses. + + Solid 1 delegates from `document` and fires any `$$click`/`$$input`/… it finds while walking up from the target, so a 1.x runtime on the same page — an older embedded widget, a devtools panel built on 1.x — ran every delegated handler in a 2.x app a second time. Compiled output and the runtime now stamp `_$$` / `_$$Data` instead; neither version can see the other's handlers, in either nesting direction. + + The key, the `_$SOLID_EVENT_OWNER` mark, and the walk rules are documented in `client.ts` as the delegated-event wire contract shared by every Solid copy on a page. Anything reading `el.$$click` directly must switch to `el._$$click`. + +- 17b0bda: Deprecate `` in favor of `dynamic()` + + `` is the same primitive as `dynamic()` with a worse shape: the tag travels in the props bag, so every instance merges `component` in at the call site, `omit`s it back out inside, and builds a fresh `dynamic()` factory (with its memo) because a JSX wrapper has nowhere to hoist one. Polymorphic-component libraries end up omitting `as`, handing the tag to ``, which merges it back in under `component` to omit it again. `dynamic()` has none of that for one extra line: + + ```tsx + // before + ; + + // after — hoist per component instance, or per module for a constant tag + const Field = dynamic(() => (multiline() ? RichTextEditor : "input")); + ; + ``` + + `Dynamic` and `DynamicProps` are marked `@deprecated` (editors strike them through; no runtime warning). They remain available in 2.0 — the RCs are past public API removals — but new code should use `dynamic()`. The migration guide and control-flow RFC are updated accordingly. + +- 1af28a1: Call `runHydrationEvents()` after `dynamic()` spreads props onto a string tag, so events the hydration script queued for a `` element are replayed instead of being dropped until `_$HY.done`. +- d2a36f5: `dynamic(source, { static })` and `isStatic(o, key)` + + `dynamic()` pays for a factory memo plus a per-instance memo so the source can + change. A great many call sites never change: a runtime `styled()` that always + renders `"li"`, and — the case this exists for — a polymorphic component whose + `as` arrived as a literal. The compiler encodes `as="button"` at a call site as + a data property and `as={isLink() ? "a" : "button"}` as a getter, so which one + the caller wrote is readable at runtime. + + `isStatic(o, key)` reads it: one descriptor lookup, no read of the value, + nothing tracked, looking through `merge()`/`omit()` views to the leaf that owns + the key. Data property or absent-from-a-fixed-key-set is static; a getter, a + store key, or a memo-backed `merge()` source is not. + + `dynamic(source, { static: true })` then says the source cannot change: it is + called once, untracked, at `dynamic()` time, and each instance renders the + result with no computation of its own — a tag goes to the compiled element path + (create or claim, spread), a component is called directly. No owner is created + on either side, so hydration ids stay aligned between server and client. A + static source may not return a promise. + + ```tsx + function Polymorphic(props) { + const Tag = dynamic(() => props.as, { static: isStatic(props, "as") }); + return ; + } + ``` + + `as` stays public and reactive; the literal case stops paying for it. Note the + two paths produce DIFFERENT hydration ids (the memo path's element sits one + owner deeper), which is fine because `isStatic` reads the same descriptors on + both sides — but it is why the classification must be per instance rather than + per component. + +- c452850: `dynamic()` / `Dynamic` with a tag-name source honor an `xmlns` prop when creating the element (#3386) + + The compiler resolves a tag's namespace from its parent at build time; the `dynamic()` runtime path creates the element before it has a parent, so a tag that exists in both HTML and SVG (`a`, `script`, `style`, `title`) was always created as an HTML element — `` with `Link = dynamic(() => "a")` produced an HTML anchor inside the SVG tree. The instance can now say which one it means with the same attribute compiled JSX uses for the same purpose: ``. Like `is`, `xmlns` is read once, untracked, at creation (the DOM can't re-namespace a node) and then applied as an ordinary attribute, so a client-rendered element carries the same attribute the server serializes. Without `xmlns` the namespace is still inferred from the tag name. Hydration is unaffected: it claims the parser-namespaced node. + + Types: `xmlns` is now accepted on the four tags that exist in both HTML and SVG (`a`, `script`, `style`, `title`) in addition to the SVG/MathML attribute sets that already had it — those are the only tags where the attribute changes what gets created, and the compiler has honored `` on them all along. Unambiguous HTML tags (`div`, `span`, …) still reject it. `dynamic()`'s tag-name components inherit it through their intrinsic attribute types. Also syncs `@solidjs/h`'s generated JSX types. + +- 53280e7: The error hooks and `SSR_RENDER_ERROR_CONTAINED` tell where an error was thrown apart from where it was met. + + `ownerPath` on `ClientErrorContext` and `ServerErrorContext` is now where the error was **thrown**: the labels root-first up the owner chain of the computation that threw — the component that broke — falling back to the boundary's chain when the throw crossed nothing the runtime could name. A new `boundaryPath` is where it was **met**: the same labels up the chain of the `` that rendered its fallback (client and server) or the `` that shipped the rejection (server, `handling: "client"`). Before, `ownerPath` was the boundary's on both sides, so every component under one boundary grouped into one path. On the client the engine's status wrapper already named the thrower (`StatusError.source`); on the server the owner scopes stamp it as the error escapes. The `SSR_RENDER_ERROR_CONTAINED` finding follows: `ownerPath` locates the throw, `data.boundary` / `data.boundaryPath` the boundary. + +- 5b31076: `ssrElement` no longer emits `style=""` / `class=""` for nullish values; they are omitted like every other attribute, matching the client. Skipped props also leave no stray whitespace in the opening tag (#3382). +- 084e621: SSR emits the `` text separator only between items that resolve to text. Adjacent memos and components that yield elements — every `Dynamic`, `Show`, or wrapper-library instance in a list — no longer carry a separator and a comment node each. Text that becomes adjacent through a nested array or a dropped nullish item is now separated, so it hydrates into distinct text nodes (#3383). +- a429b44: Server-function flight consumers now receive every mutation response that carries integration metadata — the redirect carrier or `X-Revalidate` keys (`redirect()`, `reload()`, `respond(value, { revalidate })`) — whether or not the server folded data alongside it. Metadata is envelope-level (it names what the mutation did to every cache on the page), so it reaches each registered consumer with `data` set to `undefined` where no slice was collected for that source, and the call resolves with the plain value. An integration that subscribes a consumer therefore owns redirects and revalidation without wrapping the call, and a redirect the server could collect no data for (a cross-origin target, no collector registered) navigates instead of landing on the caller as a raw `Response`. Reads (GET or `read: true`) keep the whole-response passthrough; `FlightDataConsumer`'s `data` parameter is typed `D | undefined` accordingly. + + `revalidate` gains a reserved all-keys spelling: `REVALIDATE_ALL` (`"*"`), the host-independent way to declare that every cache entry went stale, distinct from omitting `revalidate` (the host's default — Solid Router revalidates everything after an action, a router without that convention reloads only what it owns) and from an empty list (nothing). The helpers refuse `"*"` beside named keys; `ServerFunctionOutcome.revalidateKeys` delivers it to collectors as declared. + +- 549f482: Trim per-node work on the hydration claim path. `gatherHydratable` asks once whether the root contains frame regions and tests containment against that list, instead of walking every keyed node's ancestor chain with `closest("[data-fid]")`; `insert()` builds a parent's claim array in one indexed pass over `childNodes` that drops separators as it copies, instead of an iterator spread followed by a compacting pass; and `clearSnapshots` assigns `undefined` to the extension's `_snapshotValue` rather than `delete`-ing it, which pushed every hydrated source's extension object into dictionary mode. +- 3154ed6: A signal written from `onSettled` or `createEffect` during hydration no longer strands the DOM it reveals (#3504). Two halves: a root created while hydrating marks the hydration snapshot scope itself, so a write during the root pass is held until the pass completes and then replays — previously the scope was marked lazily by the first hydration-aware primitive, so a `` condition created before that sat outside it and the write cascaded mid-claim. And a streamed boundary's resume window now claims only the subtree under that boundary: a write from the resumed content that reaches a signal above it (a route's `onSettled` adding a toast to a provider) re-renders that already-hydrated region as a client render — fresh nodes, live inserts — instead of claiming against the server registry, missing with a "Hydration key miss" warning, and rendering detached. +- b298154: Move the runtimes' seams off the `solid-js` surface and behind `solid-js/internal` + + `merge()`/`omit()` returning lazy views (#3454) gave `spread()` and + `ssrElement()` a protocol for reading props leaf by leaf instead of trapping + through the proxy per key. Because `@solidjs/web` and `@solidjs/universal` + depend on `solid-js` alone — never on `@solidjs/signals` directly, so an app + holds exactly one reactive engine — every piece of that protocol went out + through `solid-js`'s main export: eleven names, `mergeSources` before them in + #3325, on the public surface with no marking. None of it is API. + + The protocol (`viewOf`, `mergeView`/`omitView`, `MergeView`/`OmitView`, + `sourceKeys`/`sourceHas`/`sourceGet`, `hasStaticKeys`, `resolvedTable`, the + `SOURCE_*` kinds, `SourceKind`) now lives on a `solid-js/internal` subpath, + along with the server-scope seams that were already `@internal` in JSDoc and + consumed only by `@solidjs/web` (`ssrHandleError`, `ssrScope`, + `runInServerComponentScope`, `inServerComponentScope`, `creationStamp`, + `getProjectionTrace`, `materializeContainerTrace`). The names stay exported + from the main entries at runtime, so the subpath shares one module state and + the single-engine guarantee is untouched; `stripInternal` keeps them out of + the generated declarations, so TypeScript no longer offers or types them. + `internal-surface.spec.ts` pins the boundary in both directions. + + Also dropped from the entries: `storeIsShallow`, `storeHasFamily`, + `storeHasOptimisticFamily` (leftovers of the gutted patch channel), + `storePath` and `$REFRESH` (referenced only by `@solidjs/signals`'s own + internals), and `NoHydrateContext` (`@internal`, used only by `solid-js`'s + server code). Nothing in the repo consumed them. + + No runtime behavior change. `merge`, `omit`, and the rest of the reactive + surface are unaffected. + +- 899c2c4: `merge()` and `omit()` are always lazy views, and props consumers read their leaves + + `omit(props, ...keys)` returns a live view of `props` for every input — a plain object included — instead of copying it with a `getOwnPropertyDescriptor` + `defineProperty` per prop. A predicate form hides keys by rule without enumerating first: `omit(props, k => k[0] === "$")`. `merge()` no longer builds an eager copy when its sources are plain objects: under `Proxy` it always returns an O(1) view over the flattened sources (a single non-function source is returned as is). + + The two compose flat. An `omit()` over a `merge()` carries one filtered view per flattened merge source, a `merge()` over an `omit()` takes the view record as a leaf, and nested omits fold their filters into one record. A component chain of `merge(defaults) → omit(consumed) → merge(statics) → omit("as")` — the shape headless-UI libraries render every element through — collapses to leaf views over the original objects, each with its accumulated filter, with no proxy layer left between the outermost spread and the author's props. `merge()` keeps the omitted keys hidden by construction (#3014) rather than by treating the omit as opaque. Construction cost drops 3–7× at depth 1–7; the SSR polymorphic-chain bench (#3448) runs ~2.4× faster. + + Reads stay cheap: a view over plain objects resolves a key → owning-leaf table once, on first read, and every `get`/`has`/descriptor is one lookup after that. `spread()` (DOM and universal) and `ssrElement()` read the leaves directly — never through the proxies' traps — and walk that table when there is one, so an effect rerun costs one read per key, as it did over the copy. Both proxies use a class target and one shared handler (no per-instance closures). + + A view over a store asks the store nothing but the read. Each source's kind (plain object, omit record, proxy, memo) is decided once, when the view is built, and carried beside it — every brand check on a Proxy is a trap (`instanceof` is a `getPrototypeOf` trap, as expensive as a store read), and store detection goes through `$TARGET`, a symbol the store's `get` trap answers on its fast path, never its generic tracked-read path. `merge(defaults, store)` constructs ~30% faster than the copy did and reads ~15% faster; `omit(store)` reads at parity. + + The views tell the truth: `Object.getOwnPropertyDescriptor(view, key)` reports a data descriptor only when the key is a data property of a plain leaf (the compiler's encoding of a static prop) and an accessor for a getter, a store key, or a memo source. Together with the new internal `hasStaticKeys()`, `spread()` now skips the children effect for static children behind `omit`/`merge` layers (#3388 through views). + + Behavior changes: + - Writes to a `merge()` or `omit()` result are no-ops (they already were for the proxy forms). A caller that needs its own object copies it (`{ ...merged }`), and the copy carries no sources (#3384). `@solidjs/html` now collects its own props and spreads into one `merge()` at the end instead of assigning onto the result. + - A data property on a source is read live through the view rather than snapshotted at `merge()`/`omit()` time. + - Key order of a merged view is the merged order — every key at the position of the last source that carries it — matching `ssrElement`'s array form. + - Sources are treated as own-keyed; a key added to a plain source after merging is not seen (the copy did not see it either). + - Enumerating a view through its traps (`for…in`, `Object.keys`, `{ ...view }`) costs a trap per key, as any proxy does; the internal consumers avoid it. Environments without `Proxy` keep the copy paths. + + Internal helpers for consumers, exported from `solid-js`: `viewOf(o)`, `mergeView(o)`, `omitView(o)`, `sourceKeys(entry, kind)`, `sourceHas(entry, kind, key)`, `sourceGet(entry, kind, key)`, `hasStaticKeys(o)`, `resolvedTable(o)`, the `SOURCE_*` kinds. + +- 3ae9e92: `OBSERVE.records` — one records channel on both platforms (observe/dev tiers); frame records from both ends; the client `"call"` record; `observeServerFunctionCalls` removed + - **`@solidjs/signals`**: `OBSERVE.records` — `subscribe(type, listener)`, `observed(type)`, `emit(type, event, live)` — the channel every runtime record rides, created once per process and registered on `globalThis` under `Symbol.for("@solidjs/signals/observe/records")` so a second copy of the core (a bundled server build instrumented through `--import`) and wire layers bundled without a framework import reach the same listener sets. Listeners are snapshotted per emit; a throwing listener is reported and the rest run. Types: `Records`, `RecordTypes` (extends `HostRecordTypes`; both declared empty, for the runtimes to augment — one augmenter per interface), `RecordType`, `RecordEvent`, `RecordLive`, `RecordListener`. Folds out of prod. New **`OBSERVE.attribution.currentOrigin()`** (and the `currentOrigin` hook on `AttributionHooks`): the provenance a root write performed now would be stamped with — the interaction whose handler is running, the navigation/effect/action frame open, or inside a recompute the origin of the change that caused it — as the engine's own `ChangeOrigin` object, `undefined` when external or with no engine; for a runtime stamping a record of its own. The installed hooks are also registered on `globalThis` under `Symbol.for("@solidjs/signals/observe/attribution")`, the same reach-without-an-import the channel has. + - **`solid-js`**: the `"boundary"` record moves from `OBSERVE.server.records` to `OBSERVE.records` (augmenting the core's `RecordTypes`). `OBSERVE.server` keeps only the `trace` slot; `ServerRecords` is gone. + - **`@solidjs/web`**: the `"invocation"` and `"frame"` records move to `OBSERVE.records` (augmenting `HostRecordTypes` through `solid-js`). New **`"call"` record** (`CallEvent`, `CallLive`, `CallListener`): one per server-function call made from the browser, at the caller's settle — `{ id, at, durationMs, method: "GET" | "POST", outcome, status?, origin?, deferred? }` with `{ args, response?, result? | error? }` beside it; joins the server's `"invocation"` by `id`, and — through `origin`, the engine's own interaction/navigation object read at dispatch via `currentOrigin()` — the attribution engine's `InteractionEvent` / `NavigationEvent` / `HoldEvent` by identity, so an observer files the call under the click that made it without a time join. The **`"frame"` record now has a client half**: `FrameEvent` is `FrameProducedEvent | FrameAppliedEvent`, discriminated by `side`, same census on both; the client half (`applyFrameResponse`, one per stream in a response) adds `address` (the `as` remap) and `outcome: "truncated"` for a body that ended before `complete`, with `live.response`. Server census fix: `regions` counts `html` chunks addressed to a child frame id (the former count read a chunk type that does not exist), and `shellMs` is set by the stream's own shell only. The emitters and their wrappers fold out of the prod client artifacts behind the observe literal (prod `applyFrameResponse` and the server-function dispatch are the pre-existing functions, no extra frame or promise hop). The server-functions and frames **client** entries gain `observe` and `development` builds and export conditions (`server-functions/dist/client.{observe,dev}.js`, `frames/dist/client.observe.js`); the server-functions client is now built with its flags replaced in every tier (before, `_SOLID_DEV_` there was an unreplaced truthy string). + - **Removed**: `observeServerFunctionCalls` and the `ServerFunctionCall` / `ServerFunctionRequestCall` / `ServerFunctionResponseCall` types, from both server-function entries. Subscribe to `OBSERVE.records` `"call"` (client) or `"invocation"` (server) instead. + - **`@solidjs/diagnostics`** (format v6): `artifact.server` is replaced by `artifact.records: { boundary, invocation, frame, call }` — always present, captured on both platforms including the browser bridge; types `BoundaryRecord`, `InvocationRecord`, `FrameRecord` (`FrameProducedRecord | FrameAppliedRecord`), `CallRecord` (with `origin?: ChangeOrigin`), `ArtifactRecords` replace the `Server*Record` / `ArtifactServer` names. JSONL: one line per record with `type` naming its table; the meta line's `boundaryCount`/`invocationCount`/`frameCount` become `recordCounts: { boundary, invocation, frame, call }`. + +- 328580f: `omit()` over a `merge()` is one record that holds the merge's record, not one leaf view per merge source + + An omit over a merge used to flatten at construction: one `OmitView` plus one combined hidden-key list per flattened leaf, and the next `merge()` copied those entries into its own arrays. On a component chain of defaults + omit + spread (Kobalte-shaped: `merge(omit(merge(omit(props))))`, four layers) that was ~19 records and as many list copies per element — the largest allocation of the render. The omit now holds the `MergeView` record itself (a new source kind, `SOURCE_MERGE`) and is one record however many leaves the merge has; a later `merge()` carries it as one entry, and a later `omit()` folds into it. Nothing is read through a proxy trap along the way: the entry helpers (`sourceKeys`/`sourceHas`/`sourceGet`, `hasStaticKeys`, descriptors, the resolved table) recurse into the record by function call. + - Reads through the nesting are one walk per read (a nested entry answers presence and value together), and a nested record counts no reads of its own toward the table threshold — the view that was asked decides for the whole tree, and its table is collected in one pass over the leaves rather than one table per layer. + - New `@internal` `sourceOwners(source, keys, owners)`: every key of a props source — a plain object, a store, or a merge/omit view — appended in merged order with the object that owns it, in one pass, later sources moving a key to the end. `ssrElement` collects any spread that is not plain objects only (a view, a store, the array form with one among them) this way, so each attribute is one direct read of its owner — no `in` walk per key through the layers, no key list per leaf, no table, and no per-entry classification (`pushEntry` is gone). + - An omit's `$SOURCES` never answers anything now (previously its filtered leaf views); consumers reach the record through `viewOf` and walk it as one filtered entry. + + Measured against `next` (interleaved, min of N, quiet machine): the tier-1 polymorphic-chain SSR harness allocates 12% less per row (14.6 → 12.8 KB) and is 2–6% faster across the interpreter, Sparkplug, Maglev and TurboFan tiers; the props-chain microbench builds 6–65% faster and builds+consumes 7–31% faster by depth and tier; the omit/merge micro-suite is flat or better in every shape. On the yak-bench SSR lanes with every yak piece on Solid primitives: +7% geomean, +20–36% on the component-composition cases (`polymorphic-chain`, `tabs`, `multifile-composition`), which brings those to parity with yak's hand-rolled runtime. + +- da6ed76: SSR: fold the text-separator entry wrappers back into `tryResolveString` and `resolveSSRNode`. #3394 split each walker into an entry function that reset the separator state and a recursive body; the bodies are too large for V8 to inline, so every template hole paid an extra call. The recursion now passes a `nested` flag instead — one function each, same output — recovering the ~2% SSR throughput that split cost on element-heavy pages (up to 7% where holes are dense). +- 5f688a6: GET-encoded server function calls no longer send the `X-Server-Function-Instance` header (#3406). A `` is reused only by a fetch that matches it exactly, headers included, so the per-call header made browsers fetch every preloaded read twice. A read's identity is now its url alone; the instance id keeps riding the POST transport, where it still names the call to the handler's hooks as `context.instance` (null for reads, as for no-JS callers). `prepareRequest` validation adapts: on a GET call the method the transport set stands sentinel for a returned init that dropped the original. +- c66130d: Remove the `X-Server-Function-Instance` header from the server-function wire protocol. Since #3094 it decided nothing: the answer shape is the address (`/data/` for the scripted transport, bare for plain HTTP), no-JS gating is the address plus a form post, and #3416 had already dropped it from GET reads so preloads could match. What remained was a per-call id the handler copied into `context.instance` for `transformResult` / `transformFlightResult`, which nothing consumed; cross-wire correlation is the trace context's job (`traceparent`, #3402). Breaking, nominally: `context.instance` is gone from the hook contexts, and `INSTANCE_HEADER` is no longer exported from `@solidjs/web/server-functions/{client,server}`. A legacy header on an incoming request is ignored, as it already was. The `prepareRequest` validation (#3174) now uses the transport's method as its sentinel on every call shape, so a hook returning a fresh `{ headers }` instead of spreading is still refused before dispatch. Client-side `observeServerFunctionCalls` events keep their local `instance` id for pairing a request with its response. +- 36db287: `renderToString` now disposes its reactive root synchronously before returning instead of via `setTimeout`, so a synchronous loop of renders no longer retains every graph until the next macrotask (#3385). The request event's response head is committed right before that dispose — the same head-freeze point an awaited `renderToStream` already uses — so `httpStatus`/`httpHeader` declarations still reach `createSSRResponse`. A render that throws leaves the head uncommitted and retracts its declarations as before. +- 61a114c: Server boundary records on `OBSERVE.server.records` (observe/dev tiers) + - New `"boundary"` record: one per `` boundary that waited during a server render, delivered when it settles — `{ id, at, durationMs, heldMs, passes, outcome: "settled" | "fallback" | "client" | "error", streamed, revealGroup?, ownerPath? }`, with the thrown error beside it. `id` pairs it with `SSR_RENDER_ERROR_CONTAINED`; `passes` counts render passes (a sequential chain reads as `3+`); under a `` group the record waits for the group's swap so `heldMs` measures how long finished content was held for its siblings. No clock is read without a listener, and the emitter folds out of prod. + - The server records channel is `OBSERVE.server.records.subscribe(type, listener)` — the server twin of `OBSERVE.attribution.subscribe(type, …)`. `OBSERVE.server.invocations` (unreleased) is renamed onto it: `subscribe("invocation", …)`. The `InvocationChannel` type is gone; `ServerRecords` is the channel's interface. + - Types now layer one augmenter per interface: `@solidjs/signals` declares `ServerObserve` empty; `solid-js` augments it with `records: ServerRecords` and `trace: ServerTrace`, declaring both; `@solidjs/web` augments those two through `"solid-js"`. `TraceSlot` in `@solidjs/web` is now an alias of `solid-js`'s `ServerTrace`. (Two augmentations of one re-exported interface through different module aliases merge order-dependently in TypeScript — one set was silently lost.) + - `@solidjs/signals` exports `ownerPath(subject)` — the root-first component-label walk its diagnostics already make — so a record's `ownerPath` and the finding it pairs with come from the one walk. + +- 0d8347a: Server records reach the diagnostics artifact and the dev checks (server-dev-build-plan P4) + - `@solidjs/diagnostics` artifact format **v5**: `artifact.server: { boundaries, invocations } | null` folds `OBSERVE.server.records` when the scenario runs under the server runtime — `captureArtifact(() => renderToStream(…))` — one row per `` boundary that waited and per server-function execution; `null` for client captures and the browser bridge. New exported types `ArtifactServer`, `ServerBoundaryRecord`, `ServerInvocationRecord` (mirrors of the runtime's `BoundaryEvent`/`InvocationEvent`; the package still depends on `@solidjs/signals` alone). JSONL egress adds `boundary` and `invocation` lines and the header counts. + - `InvocationEvent.boundary`: a direct server-function call made during a `` boundary's render pass carries that boundary's hydration id, the `"boundary"` record's `id` — the join between a boundary's wait and the calls under it. + - Two dev checks derived from the boundary facts in `ssrLoadingBoundary`: `ASYNC_WATERFALL` with `data.side: "server"` (`passes - 1` sequential flights; 2 → `info`, structured only; 3+ → console `warn`) and a new code `SSR_CLIENT_CONTENT_MASKED` (`warn`, `ssr`) for client-only content that surfaced only after a real server wait — the server's work discarded, the fallback shown for the wait. Dev tier only; the boundary clock now runs in dev without a listener. + - `solid-js`'s server `emitFinding` keeps `info` findings off the console (structured channel only), matching the core. + +- 7623ce1: Server diagnostics on `OBSERVE.diagnostics`; `OBSERVE.server` owned by `solid-js`'s server entry + + The server runtime now reports on the same structured channel as the client. **Findings** — facts about a render, present in observe and dev builds — `SSR_RENDER_ERROR_CONTAINED` (a render error a boundary routed; `data.handling` is `fallback`, `client`, or `failed` — the structured face of what `renderToStream`'s `onError` receives, on the process-wide channel), `SSR_SUBTREE_ABANDONED` (a failed fragment's pending descendants discarded), `SSR_STREAM_ABANDONED` (consumer cancelled or sink failed mid-render), `LATE_HEADER_WRITE` (recorded beside the existing dev throw / prod log), `SERVER_FN_ERROR_SANITIZED` (the original error the production wire replaced), and `FRAME_MARKER_CORRUPTED` from the frames client. **Checks** — dev-only guidance — convert every server `console.warn` to a code: `SERVER_WRITE`, `REVEAL_IN_RENDER_TO_STRING`, `LAZY_ASSET_UNMAPPED`, `PRELOAD_DESCRIPTOR_INVALID`, `HEAD_TAG_INVALID`, `BEHAVIOR_CLAIM_DROPPED`, and `UNRECOGNIZED_INSERT_VALUE` (now one code and a `render` kind on both platforms); `ASYNC_OUTSIDE_LOADING_BOUNDARY` on the server records with `data.side: "server"` before it throws. Server components are labelled for `ownerPath` (`createComponent` runs the body under a transparent `` owner in observe/dev — no hydration id consumed), so `in ` reads the same on both sides, and the server entry installs the same repair-guide console footer as the client. Prod artifacts carry none of it; `DiagnosticKind` gains `ssr`, `head`, `render`. + + `OBSERVE.server`'s objects (the invocation listener set, the trace-provider slot) are now created by `solid-js`'s server entry, once per process under `Symbol.for("solid-js/observe/server")` on `globalThis`, rather than by `@solidjs/web`'s module init: an observer's `init()` that imports only `solid-js` can subscribe and `provide` before the web runtime has loaded, and a host that bundles the runtime into its server build and instruments through a `--import`ed module finds one listener set and one provider across both copies. The core keeps `server: {}`; the client pays nothing. + +- 56858e7: The server error hook — `configureServerErrors({ onError })` from `@solidjs/web`, `onServerError` on `renderToStream`/`renderToString` and on `handleServerFunctionRequest`: the prod-tier seam through which a server integration hears every failure the runtime handles and may say what the client receives instead (sentry-integration-plan C6, #3468 part 3). + + Until now a production build reported only the failure that fails a request (`renderToStream`'s `onError`); an `` fallback rendered, a `` fragment rejected and re-rendered by the client, and a server-function throw sanitized onto the wire were observe-tier findings only. The hook is called **once per error object, at first sight**, with where the failure was met — `kind: "render"` (`fallback` / `client` / `failed`, with the boundary's hydration id and the component labels when compiled in) or `kind: "server-function"` (`thrown` / `channel`, with `functionId` and `direct` for an in-process call during SSR) — and the request event. Its return is the **wire value**: rendered into the fallback, serialized for hydration, sent as the RPC error; `undefined` keeps the default policy (generic outside dev, fidelity in dev); ignored for `failed`. A direct server-function call that throws during SSR is reported once as the function's failure, and the boundary that contains it reuses the verdict. Two tiers as `wrapInvocation` has: ambient (registered on `globalThis` under `Symbol.for("solid-js/server/errors")`, shared by a bundled build and an `--import`ed module) and per request, the latter winning. A throwing hook is reported and treated as silent. The hook fires in every tier; `onError` keeps its meaning. + + Internals: `solid-js`'s server entry owns the verdict engine (`reportServerError` / `ssrSanitizeError` through `solid-js/internal`, one cache per error object); the SSR context gains `errorPolicy` and `flushed`; the `_fr` rejection reads its verdict at delivery. A rejected async source's own serialized rejection is encoded in the rejection's microtask, ahead of the boundary, and carries the default policy's value — the mapping reaches the boundary's record, which the hydrating client renders from; no tick is added to the error path. `SSR_ERROR_SANITIZED` carries `data.wire`. + +- 537faea: `serverFunctionUrl(fn, ...args)` is now the url a `GET()` reference's own call requests — `/data/[?args=…]`, built the way the transport builds it, so a `` of it (or a prefetch, a service-worker warm, a fetch by hand) matches the later call in every cache that keys on the url (#3440). It takes the reference, like the rest of the surface, because only the reference knows the call is a GET and how its arguments encode; it throws with a pointer for a reference on the default transport (a POST is not described by its url — `invoke(fn, { priority: "low" }, ...args)` starts such a call early), for arguments that need the codec, and for a url past the length at which the call falls back to POST. + + The form-post address the function used to build — what a `
` posts to without the runtime, also `fn.url` — is now `serverFunctionActionUrl(fn | id, ...boundArgs)`, with `parseServerFunctionActionUrl(url)` as its inverse (formerly `parseServerFunctionUrl`). The id form remains for the integration that has only an id, reconstructing a callable before the declaring module has loaded. Rendered form actions and `fn.url` are unchanged. + +- af94f67: Server observe surface: `OBSERVE.server` and the invocation channel + + `OBSERVE` gains a `server` slot — an augmentable `ServerObserve` interface declared empty in `@solidjs/signals` (re-exported by `solid-js`), typed and emitted into by `@solidjs/web`'s server runtime, so server-side observability consumers subscribe on the one `OBSERVE` object they already know from the client. The first channel is `OBSERVE.server.invocations`: `subscribe("invocation", (event, live) => …)` delivers one `{ id, direct, at, durationMs, outcome, deferred? }` record per server-function execution — HTTP dispatch and direct SSR calls alike — when it settles, with the request event, `request`, `args`, and the result or the error as thrown beside it. Observers, not policy: any number of listeners, none able to alter the call; `wrapInvocation` remains the single policy hook. + + `@solidjs/web` now publishes observe-tier server artifacts (`dist/server.observe.js`, `server-functions/dist/server.observe.js`, `frames/dist/server.observe.js`) under the `observe` export condition, alongside the existing dev/prod pairs. The surface and every emit site fold out of the prod artifacts. + +- 042b540: Trace context: `getTraceContext()`, W3C `traceparent` on the exchange, and the `OBSERVE.server.trace` provider slot + + The server runtime now reads the W3C Trace Context half of the HTTP exchange once per request — continuing an incoming `traceparent` (with `tracestate`/`baggage` beside it) or originating a trace when none came in — and exposes it through `getTraceContext()` from `@solidjs/web`: `{ traceId, spanId, parentId?, sampled?, state?, baggage?, entries }`, one object per request (direct SSR-time server-function calls included), the render's own for a render outside a request scope, `undefined` outside both and on the client. Application code forwards a trace downstream with `entries.traceparent`. This is core HTTP behavior in every build tier. + + The trace is also handed down to the browser: `entries` are emitted as `Server-Timing` metrics (`traceparent;desc="00-…"`) when the response head commits — `createSSRResponse`, `commitEventResponse` (now also for an event without a response stub, such as the server-function handler's default event), and `commitResponseStub` (which accepts the owning `event` in its options) — and as `` tags in the HTML shell head, delivered wherever the head content goes (`` splice, `onHead`). A `Server-Timing` name the application already wrote is respected; `Server-Timing` now folds entry by entry when a stub and a response/`responseInit` both carry one. The browser is told only when something is recording the trace — the incoming `traceparent` was sampled, or a provider answered — never for a trace the runtime originated alone or an unsampled upstream one (what load balancers and meshes stamp on every request), so an app with no APM sees zero wire change. + + In observe/dev builds, `OBSERVE.server.trace.provide(provider)` installs a single global provider whose answer merges over the derivation (fields replace, `entries` merge by name) — how an APM's server SDK contributes its active span and vendor entries (`sentry-trace`/`baggage`) once, with no per-request entry point into the host. + +- 7f6332a: `spread()` creates fewer reactive nodes per element (#3388): `ref` folds into the attribute effect and is re-applied only when its identity changes (refs run with no owner, so nothing they create is disposed by the fold); children keep their own owned `insert` — that effect owns the child subtree, and merging it would rebuild the children on every attribute change — but a plain object whose `children` is a data property inserts the value with no effect at all. Three nodes become two when children flow through the spread, one when they don't. `spread` also accepts an array of sources with an optional `skip` predicate — `spread(el, [a, b], skipChildren, skip)` — the union of own keys with later sources winning, only the winning source read, function sources called inline with no memo (and so no hydration id), matching the server `ssrElement` array form. +- a732d2b: `ssrElement` serializes the common element shapes without the general resolver's allocations + - Children that are one string, a number, nothing, or one finished node now join the open and close tags in place. The general path — `resolveSSRNode` into a fresh `{ t, h, p }` result (an object and three arrays), then `ssr()` over a template array — is taken only for arrays and pending nodes, which need it. Output is unchanged; on a text-content-heavy page this was the single largest cost of a spread element. + - The array-sources form over plain objects, and an omit over a merge, walk the array they were handed: the resolved `sources`/`kinds` lists (and the `fill(SOURCE_OMIT)` list) are built only when entries differ in kind. A plain source in `pushEntry` is classified with one `$PROXY in` check instead of two. + + On yak's element-dense SSR cases (`dyn-translate`, `dyn-fair`, `dyn-inline`) this is +15–21% throughput and −27% bytes allocated per instance, closing the gap to the hand-written writer from 1.76× to 1.46×; `multifile-composition`/`tabs` +3–6%. + +- 40977c9: `ssrElement` accepts an array of prop sources and an optional `skip` predicate: `ssrElement(tag, [a, b, c], children, needsId, skip)`. The array form serializes straight from the sources with the exact output of `ssrElement(tag, merge(a, b, c), ...)` — later sources win per key, attributes land in merged order, only the winning source's getter is read (once), and `skip(key)` drops a key from every source without reading it — so libraries and compilers spreading several sources no longer have to build an intermediate merged object that is walked once and discarded. The array (or a thunk yielding it) is resolved after the hydration key is taken; a function source is a plain thunk called once that creates no memo and consumes no hydration ids. Nullish sources are empty. The single-object form is unchanged. +- 34287d8: SSR render failures reach the client sanitized (#3468): the wire policy the server-function handler has applied since #3113/#3116 now covers every SSR road a failure takes — the error an `` serializes for hydration, a rejected async source serialized into the stream, a `` fragment's `_fr` rejection, a frame stream's error chunks (the fragment's, a live hole's, the root's). Outside the dev build a plain thrown value is replaced with a generic `Error` (`"Internal Server Error"`); `message`, `cause` and own properties stay on the server. A `"use server"` function called in-process during SSR never touches the RPC wire, so before this the production page load shipped what that wire withholds. + - **`solid-js`** (server): `` sanitizes _before_ rendering its fallback and serializes the same replacement, so fallback markup and the hydration record agree. A fallback printing `err().message` shows the generic message in production, as for a server-function failure. `markSafeError` (`Symbol.for("solid.SafeError")`) passes through with own properties; an Error reached as a _value_ is data and passes as written (#3113's ruling). One replacement per original, however many roads it takes. New finding `SSR_ERROR_SANITIZED` (`info`, observe + dev; `data.error` the original), beside `SSR_RENDER_ERROR_CONTAINED` which carries the failure itself. `ssrSanitizeError` is exposed to the runtimes through `solid-js/internal`. Server findings now carry their `ownerPath` in the **observe** artifact too — the core's walk reads `_parent` under its own build's property mangling and found nothing on a server owner there, so the server entry locates its findings itself. + - **`@solidjs/web`** (server): the hydration serialize funnel guards every channel it writes (a promise's rejection, an async iterable's thrown step); a fragment's terminal error reaches the `_fr` rejection and a transport sink's error chunk sanitized while the abandonment ledger keeps the original; the frame sink's root error chunk and the live-hole/live-attribute error chunks carry the replacement's message. + - **`@solidjs/signals`**: the `SSR_ERROR_SANITIZED` code. + + The dev/prod line is the build variant: the `development` server artifacts keep full fidelity; production and observe sanitize. + +- 48f007e: `renderToStream` completes when a nested `` settles before its parent fragment fails (#3478). A settled nested fragment parks its markup on the pending parent to be spliced in on resolve; when the parent then rejected, the splice ran over an undefined value, threw out of the fragment's resolver before `_fr` could reject, and the response waited on it forever. The error path now drops the parked children — the client re-renders the whole subtree off the outer rejection — and the failure is reported once. +- 7f5f902: `merge()`/`omit()` views build their resolved key table on enumeration or once reads have paid for it, not on the first read; `ssrElement` walks a view's entries instead of asking for its table + + A `merge()`/`omit()` view over plain objects keeps a resolved table — every key mapped to the leaf that owns it — so a client `spread` rerunning its effect, or `Object.keys`/`{...props}`, is one lookup per key. Since the views became lazy (#3454) that table was built by the **first** per-key trap read as well. On the server that is the wrong trade: a component reads its merged props a few times, the element serializes them once, and the view is discarded, so a Kobalte-shaped component chain (`Dialog.Trigger` → `Button.Root` → `Polymorphic`) paid for a table per layer per element. Profiled under `renderToString`, a third of the time was in the table code (`mergeTable`, `tableSet`, `omitTable`) and the garbage it produced, plus `Array.prototype.concat` combining omit filters. + - **Signals:** a per-key `get`/`has`/`getOwnPropertyDescriptor` answers by a source walk (last source first, one `in` each) until the view has been read 16 times — the break-even between a build (~60 ns per key of every leaf) and a walk (~20 ns per source) — and then builds the table as before, so a long-lived client view read on every reactive rerun is one lookup per read from its first few updates on. Enumeration (`ownKeys`, or a consumer asking `resolvedTable`) builds it outright, unchanged. An omit over one object never builds one. The read count lives in the table slot until it is decided, so a view carries no extra field. Combined omit filters are copied with `slice` + `push` instead of `concat` (2–3× cheaper once optimized, and — unlike a hand-written loop — no more expensive than the builtin in the interpreter and baseline tiers, which is what an instruction-count benchmark under Valgrind mostly runs). + - **Web (SSR):** `ssrElement` no longer prefers a view's resolved table; it walks the view's entries the way it already walked the array form — an omit over a merge as its filtered leaf entries — so serializing an element builds no table at all. Attribute order is the same merged order (a key at the position of the last source that carries it). + + Same-process A/B on the tier-1 `polymorphic-chain` SSR bench (200 rows, `renderToString`): the chain form goes from 8.2× the compiled floor to 5.8×, chain-static from 7.7× to 5.6× (−27% wall-clock). At the signals layer the props-chain bench improves 5–17% on build, 6–14% on build+consume, and steady-state per-key reads on a prebuilt view are unchanged (the table is in use). The only slower band is a view read 16–20 times and never enumerated (+4–17% at depth 1–3, faster at depth 7), which is the transition the threshold is designed around. + + Tests: the table is undecided after a handful of reads on both a merge and an omit-over-merge, built after the 16th with identical answers before and after; enumeration builds it on a fresh view; a plain omit never builds one; a store-leaf view settles to "none" and keeps walking. SSR: a spread over an omit-over-merge and over a bare merge serializes the merged order and builds no table. + +- Updated dependencies [8bf04ea] +- Updated dependencies [d826cd3] +- Updated dependencies [cc0396b] +- Updated dependencies [53280e7] +- Updated dependencies [3154ed6] +- Updated dependencies [b298154] +- Updated dependencies [899c2c4] +- Updated dependencies [3ae9e92] +- Updated dependencies [328580f] +- Updated dependencies [61a114c] +- Updated dependencies [0d8347a] +- Updated dependencies [7623ce1] +- Updated dependencies [56858e7] +- Updated dependencies [af94f67] +- Updated dependencies [e87d694] +- Updated dependencies [5426ffb] +- Updated dependencies [63560a1] +- Updated dependencies [34287d8] + - solid-js@2.0.0-rc.9 + ## 2.0.0-rc.8 ### Patch Changes diff --git a/packages/web/package.json b/packages/web/package.json index 8778cb39c..56fb7ce59 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,7 +1,7 @@ { "name": "@solidjs/web", "description": "Solid's web runtime: client rendering, hydration, SSR, and DOM-specific control flow (Portal, Dynamic).", - "version": "2.0.0-rc.8", + "version": "2.0.0-rc.9", "author": "Ryan Carniato", "license": "MIT", "homepage": "https://solidjs.com", @@ -319,7 +319,7 @@ "seroval-plugins": "~1.6.7" }, "peerDependencies": { - "solid-js": "^2.0.0-rc.8" + "solid-js": "^2.0.0-rc.9" }, "devDependencies": { "@codspeed/vitest-plugin": "^5.4.0",