Fix 3133 flatten array - #3142
Conversation
…spec, internals, transition pins. (1) SPEC-ASYNC-SEMANTICS.md gains A27: as written, A19's definition (pending ≡ observable value not final) plus exception 1 (only *uninitialized* sources are loading-class) would have classified the open loading window as pending via cause (ii) — the spec contradicted the shipped verdict-quiet behavior. A27 rules the commit-#0 window loading-class on all three axes (reads serve commit #0, transitions never initiated/extended, verdict quiet at every distance in both forms), frames it as A19's question scoping applied (answered by declaration — re-ask-shaped, A24 family) rather than a new exception, records why true was structurally unavailable (pending is chain-shaped; no held commit to shadow; a point verdict cannot propagate without rebuilding the machinery the window silences), and notes A25 unchanged for plain seeds — seedLoadingValue is the author promoting the draft to commit #0. A19 gets a one-line cross-reference. (2) INTERNALS-ASYNC-STATE.md §1 gains the _loading row (the only node field the table was missing): set at birth, invisible to the verdict path by construction, cleared by first landing on any path, kept by real errors. (3) The one untested claim in the matrix — "the window never holds a transition" lived only in comments — is now pinned by two tests: writes concurrent with an open window commit ambiently while the identical write pair after close is transition-held (the loading-class/pending-class contrast in one test), and a loadingValue node mounted inside a live transition renders commit #0 immediately and adds nothing to what the transition waits for (its never-landing flight would deadlock the completion check if the window were pending-class). Both passed first run — pins, not fixes. 27 tests green. No changeset: docs and tests only. Co-authored-by: Cursor <cursoragent@cursor.com>
…dingValue are stripped by the hydration-aware wrappers until SSR renders commit #0. The server ignores loading values today (an async source suspends into its Loading boundary and streams the REAL value), so a node hydrating with an open loading window computes structure from the placeholder while claiming DOM the server rendered from real data — a Show over data.skeleton claims the wrong branch and corrupts the walk (pinned: the parity harness's streamed replay produced exactly this against the un-stripped build — "Hydration tag mismatch: expected <i> but found <b>item-0" — before the guard went in). The strip (stripLoadingValue, applied in hydratedCreateMemo/Signal/Optimistic and the store-like wrappers) makes a hydrating node adopt the serialized server value exactly like any async source; loading values apply only to fresh client mounts, where commit #0 is correct by construction — server HTML and hydrating client then agree by construction in both replay modes. Two parity scenarios pin it (loading-value-memo, loading-seed-store), each shaped so the placeholder flips a Show branch: a regression corrupts the claim rather than merely mismatching text, and both run in loaded and streamed modes with the harness's generic invariants (no warnings, no client-created nodes, node identity, update pass). Verified: solid-js 502/502, solid-web client 404/404, hydrate 101/101 (65 parity), server 240/241 — the one failure is retry-robustness's "root hole (no boundary)" timeout, reproduced identically on clean next (pre-existing, tracked separately). The real SSR story for the feature (server renders commit #0 into HTML, keeps streaming the landing as data, hybrid collapse) is the agreed follow-up that replaces this guard. Co-authored-by: Cursor <cursoragent@cursor.com>
…nder on the server and hydrate as the claimed value, replacing the interim hydration strip. The server never suspends a loading-value source: processResult serves the loading value instead of NotReadyError on every async path (thenable, iterator, hybrid, NotReady-retry from an unready sync dep, ssrSource "client"), the boundary never trips, markup flushes from commit #0, and the landing streams as data through the existing serialization channel. The first-value lock generalizes to commit #0: on serialized paths the HTML-visible value never advances past the loading value (settle callbacks skip the comp.value write; projections mark the pending proxy ready immediately, retargeted at a frozen seed copy, and the V1 freeze skips retargeting), so later-rendered holes in the same response can't tear against already-flushed placeholder markup — landings live in the data channel, exactly as later iterator yields always have. One deliberate exception to "sync results are never serialized": a sync landing after a NotReady retry ships as a resolved promise, because placeholder markup is already on the wire and the client can't re-derive its way out of DOM claimed against commit #0. createSignal's server fn-form forwards loadingValue (it previously dropped everything but deferStream/ssrSource); ServerSsrOptions gains seedLoadingValue and createProjection applies it across all four async branches. Client side, the strip (stripLoadingValue) is deleted: hydrating nodes are born committed with the loading value so the claim matches the placeholder markup by construction — and readHydratedValue gains the inverse guard the loaded replay exposed: a settled serialized landing (s===1 stamped ref) must NOT unwrap synchronously for a loading-window node, or the client computes real-data structure during the claim walk against placeholder DOM and corrupts it (the exact mirror of the pre-strip streamed failure). It hands the async runtime a clean thenable instead — commit #0 serves through the synchronous walk, the landing applies on the following microtask, the acknowledged loaded-mode tradeoff. Parity scenarios flip from pinning the strip to pinning the design: shells now assert placeholder markup (skeltail/emptyend), settled text asserts the landing, and the update pass pins post-hydration refetch. Verified: signals 1225/1225, solid-js turbo 26/27 tasks — the one failure is solid-web's pre-existing retry-robustness timeout, reproduced identically on clean next; hydrate parity 65/65 both modes. Co-authored-by: Cursor <cursoragent@cursor.com>
…arity matrix, two hydration fixes, commit-#0 types, and a demo page. Six new harness scenarios extend the two existing ones into the full matrix (iterator memo, ssrSource "client" memo, hybrid iterator memo, generator projection, "client" seed store, hybrid seed store — each placeholder flips a Show branch so a window disagreement corrupts the claim structurally), and the matrix immediately caught two real bugs. (1) A fully-buffered iterator replay (loaded mode) delivers its first yield synchronously via syncThenable, closing the window mid-claim — the client computed real-data structure against placeholder markup ("expected <b> but found <i>iter-final"). normalizeIterator now defers the first yield one microtask when the node has a loading window; sync delivery stays for windowless nodes, whose claim NEEDS the value. (2) The buffered store replay applied the first-yield snapshot synchronously at claim — right for windowless stores (the snapshot IS what the SSR DOM shows), wrong for a seed window (the DOM shows the SEED): after the rebase onto next picked up the backlog-parking fix (a37611e), the snapshot now parks with the backlog until hydration completes, closing the "emptyiter-bend" partial-claim corruption. The hybrid seed-store scenario also documents a pre-existing hybrid constraint: promise-shaped takeover derives hand values back by RETURNING them — draft mutations on the takeover run go to the discarded shadow draft — and with the deferred serialized adoption superseded by the takeover flight, the takeover landing is what closes the window. Types learn commit #0: loadingValue overloads on createMemo/createSignal/createOptimistic in both runtimes drop undefined from the accessor — including ssrSource "client", where the server now flushes the loading value and the client serves it pre-compute — and type prev as T, matching the signals core; the client-mode scenario drops its cast to pin the inference. The rendering example gains a Skeleton page (lazy route) demoing the basic pattern: value-channel skeleton on first flight (no Loading boundary on the page), isPending-driven dim on refetch, fresh window per client navigation; verified live against the streaming dev server — the shell flushes the skeleton markup and the post-</html> chunk is a single settle script carrying the landing as data. Verified: turbo 26/27 (the one failure is the pre-existing retry-robustness timeout on next), parity 79/79 both modes, solid-web test-types green, example typecheck green.
Co-authored-by: Cursor <cursoragent@cursor.com>
…ateStore + seedLoadingValue renders beside the createMemo + loadingValue card, sharing one Refetch and one isPending dim (memo pending OR store-read pending), so both commit-#0 forms are visible in initial SSR and on client navigation. Verified live against the streaming dev server: the shell flushes both skeleton cards (8 skeleton lines, two aria-busy), both landings travel as serialized data only. The seedLoadingValue casts in the parity scenarios were stale, not load-bearing — ProjectionOptions (client) and ServerSsrOptions (server) both type the flag now — so all four drop; the remaining casts are the async-generator computes. solid-web test-types and example typecheck green.
…ry — no Show, no fallback tree. The loading value is now data shaped like the answer (a feed whose items haven't arrived: placeholder rows with empty text), rendered by the exact same FeedCard/For template as the landed data; CSS paints rows with the placeholder flag as shimmering blocks. This is the pattern the feature exists for — commit #0 flows through the value channel and the one template, instead of hand-rolling the branch-to-a-fake-tree shape that Loading already does better. Verified via streaming SSR: shell carries both aria-busy cards with placeholder rows, landings ride the data channel only.
… data through the real template, not a skeleton costume. Gray shimmer rows still read as a fallback; per the critique that spawned the feature ("show the UI with default data with a loading indicator", "use the real graph component as the loading skeleton but with dummy data"), the loading value is now dummy items with the real items' shape and sentence structure (Shipped release #—), rendered by the same FeedCard with normal typography, and the affordance is encoded in the data itself: a provisional flag drives dimmed text and an inline pulsing dot, nothing structural. Verified via streaming SSR: dummy items and provisional classes in the shell, the landing rides the data channel only.
…t, unconditional clears; the signals gates ratchet for the feature. The unready-source parking sequence (blocked + addPendingSource + setPendingError) dedupes into parkLoadingWindow, shared by recompute's catch (sync dependency throw) and handleAsync's handleError (NotReadyError-rejected flight); recompute's catch hoists its four instanceof NotReadyError tests into one boolean; the six window-clear landing sites drop their read guards (an unconditional store to an always-present boolean slot is smaller than check-then-write and semantically identical). The hydration entry stops precomputing hasLoadingWindow per wrapper — options thread through readSerializedOrCompute into readHydratedValue, which checks at read time (hydration-only reads, trivial property probes). Measured effect is honest but small: core floor 7.19 -> 7.18 KB brotli, minimal-app 9.84 -> 9.81; the bulk of the feature's ~110 B is ~15 already-minimal sites on always-retained memo paths, and the structural outs don't exist — the window can't ride STATUS_UNINITIALIZED (born-committed flags 0 is the invariant that keeps isPending false and transitions closed) and null-slot hooks can't shake because loadingValue is an option, not an import. Per the size-config's own convention the two breached gates ratchet with the reason recorded inline: core floor 7.1 -> 7.35 KB (measured 7.18), isPending/latest 8.75 -> 9 KB (measured 8.84); createStore (12.99/13.15), minimal (9.81/10), CSR (11.96/12) hold. scripts/size/package.json records the esbuild postinstall approval newer npm requires. Also: the demo page's aria-busy takes the enumerated "true"/"false" form and the class object coerces the provisional flag to boolean (turbo's typecheck against freshly generated types caught both). Verified: turbo 26/27 (the one failure is the pre-existing retry-robustness timeout), hydrate suite 118/118 directly, all five size gates green.
…ured on the rebased base. With the store-engine seam's new hydrating scenarios (28f7bec) now measuring the feature's hydration guards for the first time: no-store 16.15 -> 16.35 KB (measured 16.04, +210 B over the seam landing — the core window plus the claim-walk guards on the shared signal-hydration body: the clean-thenable unwrap guard, the deferred first yield, the hasLoadingWindow probe), with-stores 23.05 -> 23.3 KB (measured 22.83, same bytes plus the store-replay seed parking); reasons recorded inline per convention. Full picture against clean next: core floor +110 B, createStore +120, isPending +140, minimal +130, hydrating +210/+210, CSR +90 — all seven gates green. Verified on the rebased base: turbo 26/27 (pre-existing retry-robustness timeout only), server 245/246, hydrate 118/118. Co-authored-by: Cursor <cursoragent@cursor.com>
The server cannot run a client source, so what the pre-compute window renders must be the author's explicit call, never an implicit promotion of undefined/the seed into an observable value. Signal-family sources declare loadingValue (explicit `loadingValue: undefined` is a real declaration — the undefined goes in the type); store-family sources promote their seed with `seedLoadingValue: true`; effects are exempt. Type-level: the bare "client" overloads are removed from both entries (memo/signal/optimistic), and the store families' client options now require `seedLoadingValue: true` (HydrationClientProjectionOptions / ServerClientStoreOptions). Runtime: assertClientCommitZero names the fix — behind IS_DEV on the client (zero production bytes; prod falls back to the previous gate behavior), always-on in the single-build server entry where the promotion would flush into markup. Portal's internal client-sourced memos declare `loadingValue: undefined` — "server renders nothing" is their honest commit #0. Tests and scenarios migrate the same way; new dev-error suites pin the message on both entries. Size gates: all seven unchanged against the stashed baseline (measured byte-identical). Turbo 26/27 (pre-existing retry-robustness timeout only). Co-authored-by: Cursor <cursoragent@cursor.com>
The gate compute's synchronous prev-return counted as the node's first real answer: recompute's sync path cleared _loading, so the post-hydration compute — the node's actual first question — ran pending-class (isPending true), unlike the identical source on a fresh CSR mount, whose first flight is verdict-quiet under the window. The ruling is that a loadingValue node's first flight never reports pending; the gate was answering the question with commit #0 itself. The gate now returns UNASKED — a shared never-settling thenable — for loading-window sources. handleAsync subscribes, nothing lands, and the existing `if (el._loading) return el._value` path serves commit #0 with no transition and no NotReadyError; the gate flip's recompute replaces _inFlight, so the callbacks can never fire stale. Both gates: hydrateSignalLike (memo/signal/optimistic) and hydrateStoreLikeFn (store/projection/optimistic-store, keyed off seedLoadingValue). Non-windowed sources keep the prev-return. New unit tests pin the full arc for both families: commit #0 + isPending false during hydration, still both during the first real flight, landing commits, and the SECOND flight is pending-class like any refetch. Cost: +41 raw bytes in hydrating bundles only (~10-20 B brotli, gates hold with no ratchet); CSR/minimal/signals bundles are byte-identical — the ±30 B brotli wiggle on the CSR gate is minifier name-assignment reshuffling, verified by diffing saved bundles. Verified: solid 513/513, hydrate harness 118/118, web 404/404 direct (the turbo failure is the pre-existing retry-robustness timeout). Co-authored-by: Cursor <cursoragent@cursor.com>
…tags, response holds creationStamp() counts owner creations so the live-holes engine can latch holes whose evaluation builds reactive scopes (impurity gate); Loading and error boundary output accessors carry $lhSkip so boundary lifecycles are never intercepted as re-runnable holes; the async-iterable memo pump holds the response window open (ctx.hold) until the iterable finishes, so iterable-fed holes stream every value. Chat demo streams markdown token-by-token through a live innerHTML hole, with integration specs pinning the chunk sequence. Co-authored-by: Cursor <cursoragent@cursor.com>
…, lifetime, attr cells Co-authored-by: Cursor <cursoragent@cursor.com>
…client pump, t=0 matrix rows Solid half of the reactive pole's t=0 face. Server: inServerComponentScope reads the server-component context barrier — the document face arms one live-hole engine render-wide but only holes inside a component's scope may mark and bind, and the iterable-memo pump takes the same gate (an iterable in app-level NoHydration content has no live holes reading it; pumping would hold the response for nothing). Client: the frames consumer pumps the document's ONE sc:live record — a module-level reader broadcasting ops to every adopted boundary at its own address (version 0, so a call-driven stream's apply supersedes and document ops go quiet). Page geometry routes: hole ids are document-unique and each frame's apply searches only its own range. An op log replays to boundaries that adopt after ops arrived — registration and replay are one synchronous span, so the log is exactly the pre-adoption history and store idempotence covers the overlap. Matrix: t=0 × live holes rows — in-place morph and attr patch on adopted content, catch-up after late adoption, supersession — plus the real-core producer spec pinning the context-clone geometry (two components under wrappers share one channel and the response still completes) and the hold-latch lifetime (an iterable-fed hole's response ends at completion). Co-authored-by: Cursor <cursoragent@cursor.com>
The assistant is already typing as the page loads: welcome() is a server component rendered into the INITIAL document (dynamic() over the direct call at App setup), so generation starts with the page and its markdown streams over the document's own response. First tokens paint through the streamed fragments before any JavaScript runs; later yields ride the sc:live channel and the catch-up replay applies them when hydration adopts the boundary — mid-generation, zero network. The reply() calls below it are the same component machinery on the call-driven face, so one page demonstrates both transports. The greeting reuses the model driver with a faster cadence (the document response window holds open for exactly the generation), and its text narrates what it is demonstrating. Co-authored-by: Cursor <cursoragent@cursor.com>
…x rows
A channel slot op (a re-emitted occurrence record from the document arg
ledger) updates the adopted occurrence's live props in place. Unlike hole
and attr ops, slot ops are store-keyed — two boundaries can share an
occurrence name — so they carry the producing frame's id and only the
owning boundary applies them. Real-core producer spec pins the natural
authored crossing (<props.status text={text()}/> over an iterable memo);
matrix rows pin the consumer half and the fid gate.
Co-authored-by: Cursor <cursoragent@cursor.com>
The status args now cross in the natural authored form (progress={progress()},
stats={stats()} over async-value memos) instead of asyncArg: the demo shows
BOTH live representations at t=0 — markup holes (Part markdown morphing per
yield) and expression slot args (record re-emissions over sc:live), with the
per-arg pending story (stats settles at generation end, covered by the fill's
own Loading). Real-core specs pin per-arg pending and fill mint-suppression;
matrix rows added.
Co-authored-by: Cursor <cursoragent@cursor.com>
The audit found the frames client re-shipping seroval eagerly (the data tables imported createJSONDataTable statically), defeating the lazy-codec transports: 16.8 -> 10.9 kB gz eager, codec (13 kB) loads on first data chunk via the host's prepareData hook. The sc:live catch-up log also compacts per target (ops are last-value-wins) instead of growing for the page's life. New size scenario pins the eager frames entry at 10.35 KB with the codec external. Types pipeline reordered: the frames tsc now reads the serialization types, so the copy step runs first. Co-authored-by: Cursor <cursoragent@cursor.com>
Publishes the runtime's serializer decode split as @solidjs/web/serialization/decode and points every read-only late-load at it: the frames client's prepareData codec and deserializeStream's decode half resolve there, so the hydration Serializer and toCrossJSONStream stay out of browsers that never encode (~6.5 kB gz lazy chunk instead of ~13). Rich-args encoding still loads the full serialization entry. The full entry's surface is unchanged — it re-exports the decode module. Co-authored-by: Cursor <cursoragent@cursor.com>
Projections passed as slot args to server components cross as bounded
async traces — one snapshot, then PatchOp batches — and materialize on
the client as live read-only projections.
Server (solid-js): projection trace registry (getProjectionTrace) over a
multi-consumer shared pump — one source iterator drives an append-only
patch log; hydration resume and every slot crossing subscribe and replay
from their own cursor, with snapshots captured only at stable pull
boundaries so undrained writes can't double-apply.
Client (solid-js): materializeContainerTrace — a projection fed by the
trace under its own root. The container REFERENCE is available
synchronously; reads INTO it suspend until the snapshot (the fill's own
<Loading> covers them, same contract as the value tier), patch batches
apply through applyPatches, the trace's end latches the last state.
Frames client (@solidjs/web): installs the materializer, revives
document-face { $tr, $ta } marker literals at arg-read (host revive),
and classifies containers FIRST, trap-safe — the slot props proxy
returns the store instead of async-probing it (a pending projection's
.then probe throws not-ready), and the record-dedupe compare
identity-tests containers through the host isContainer hook. Both gaps
were surfaced by the new lifecycle-matrix rows (container-args.spec:
stream face, shared identity, t=0 adopted markers), which pin the
client-pipeline halves end to end.
Size: frames eager consumer 10.37 KB measured (+19 B for the eager
halves — materializer install, marker reviver, WeakSet probe; the
seroval plugin itself rides the lazy codec chunk), limit 10.4 KB.
Co-authored-by: Cursor <cursoragent@cursor.com>
…tial-stream smoothing The demo now covers all three DR-2 tiers live: `usage` passes a projection whole across the slot border (case 3) and <Status> reads its materialized twin like local store state, alongside the expression-tier progress/stats. The markdown pipeline gains the two things a real LLM UI has — syntax highlighting (highlight.js, a server-only dep whose sole client artifact is a CSS token palette) and a close-unfinished-constructs pass per yield (dangling **/*/backticks balanced, half-arrived links hidden; fences need no help — unclosed is spec-legal code-to-end). Autoscroll moves to onSettled with a bottom-pin that respects the reader scrolling up, and the signals canned answer shows 2.0's split compute/effect form. Co-authored-by: Cursor <cursoragent@cursor.com>
…ne-projection latching Companions to the live chat vetting: hydrating a client fallback over an adopted region must not orphan the server's settled nodes (the insertExpression current-honesty fix in dom-expressions), onSettled must fire after hydration's first stable render, and a projection minted inline in a slot-arg expression latches instead of re-emitting a fresh trace per commit on both faces. Co-authored-by: Cursor <cursoragent@cursor.com>
…it .js extension, untyped slot props record) Co-authored-by: Cursor <cursoragent@cursor.com>
… {}`, compiler override drops
next.23 split the plugin's `ssr` option: it is a boolean again ("is the
app server-rendered") and the turnkey generation (entries, document
shell, serving layer) lives under `start: {}`. All four SSR examples
migrate. next.24 also pins the native compiler at next.40, so the
workspace override that held it there (the #2959 condition-memo
symmetry fix) is no longer needed.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
RFC 05 positions the option as an escape hatch for provisional-data UI — Loading boundaries stay the primary pattern — and the ssrSource "client" bullet cross-references it instead of explaining commit #0 inline. Co-authored-by: Cursor <cursoragent@cursor.com>
…s, quiet landings (#2988 #2989 #2990) Three holes in the loading-window contract, reported against the shipped feature: - #2988: a seed-window projection's derive now works a detached shadow of the seed — draft writes before an await or between yields land on the shadow and stay invisible until a commit point reconciles a detached snapshot into the store. The server freezes its seed copy before the derive runs, so SSR's commit #0 is the pure seed too. - #2989: parking a retry on a node whose window already settled to an error no longer clobbers that error with a NotReadyError — the settled error stays the answer until a retry can actually run, on both the park and the source-settle paths. - #2990: the window closes when the first answer becomes OBSERVABLE, not when it computes. Direct commits close it immediately; transition-held landings (asyncWrite holds, held sync recomputes) keep it open until commitPendingNode commits the hold, and the verdict's held-value branch is window-gated — so live observers never see a one-frame isPending pulse between the landing and its reveal. Co-authored-by: Cursor <cursoragent@cursor.com>
…override drop assumed 8d9bd0a dropped the workspace override forcing @dom-expressions/compiler to next.40 ("Drop once vite-plugin-solid bumps its pin") and bumped the examples to next.24, but the root devDependency — the one solid-web's vitest harness compiles with — stayed at next.21, whose native compiler pin (next.34) predates the #2959 condition-memo symmetry fix. The cond-component-prop parity scenario failed exactly as the removed comment predicted: conditional component props hydrate with drifted ids under the old compiler. next.24 pins compiler next.40; the scenario and the full suite are green (27/27 turbo tasks). Also commits the regenerated loading-seed-iterator-store artifact: runtime next.41's serializer emits async generators as null-prototype objects with Symbol.toStringTag, a benign output change the artifact records. Co-authored-by: Cursor <cursoragent@cursor.com>
The chat example postdates the beta.29 "stop versioning the example apps" ignore list, so changeset version was bumping it as an internal dependent (0.0.1-beta.0 + a dependency-only CHANGELOG). Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The scripted redirect mask exists because fetch follows redirects before the transport can read them, so it now covers exactly the statuses fetch follows (301/302/303/307/308, Fetch §2.2.3) — a 304, the natural answer for a conditional read, forwards untouched for every caller. Returned envelopes keep their status for unscripted callers (the returned path hardcoded 200 where the thrown path forwarded it), and the no-JS form convention honors a returned redirect envelope's Location the way it already honored a thrown one. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Mutations feeding live sources (sockets, subscriptions, live queries) have no response to await; until(fn) resolves when a reactive predicate settles truthy, so `yield until(...)` holds the action's transaction — and its optimistic state — open until the world confirms. Predicates read the authoritative view: the caller's own optimistic overrides are invisible (no self-ack), while transition-staged truth reads normally (refusing it would deadlock the hold on its own data plane). Timeout/abort reject at the yield point, reverting like any failed action. Also fixes resolve()/until() stale delivery when a source settles into a held transaction: promise-delivery effects now commit their value directly (CONFIG_DIRECT_COMMIT), keeping value and delivery on one schedule. Co-authored-by: Cursor <cursoragent@cursor.com>
The client classified any status >= 500 as failure, so respond(value,
{ status: 500 }) threw a synthesized error with the author's value
discarded. Among responses the runtime encoded (body format present)
the status is the author's data channel: only the protocol's error tag
rejects now, and a peer's own 5xx (no body format) is still refused
before decoding. The server agrees from its side: a plain thrown error
answers a real 500 instead of 200-with-tag, so intermediaries see what
the tag tells the client.
Co-authored-by: Cursor <cursoragent@cursor.com>
…#3094) One url served two answer shapes — codec encodings for the client transport (keyed on the instance header), plain HTTP for everyone else — and shared caches key on the url alone, so one caller kind's cached answer could be replayed to the other. Scripted calls now go to <endpoint>/data/<id>; the bare <endpoint>/<id> stays plain HTTP (a reference's .url, rendered form actions, direct callers). The shape is a function of the url, never a header. Transitional: the instance header still summons the scripted shape at the bare address so loaded tabs survive a deploy, with those answers forced no-store. Co-authored-by: Cursor <cursoragent@cursor.com>
…#3100) The reconnect loop drew the definite-rejection line at "below 500", so a rate limiter's 429 or a gateway's 408 permanently closed a healthy stream — the statuses most likely to be transient, and least likely to be the application's own answer. 408/425/429 now reconnect like a 5xx, as does any failure carrying Retry-After: the peer naming the wait has answered the question the exponential backoff guesses at, so the named wait (capped at 60s) replaces the guess for that attempt, and the header value rides the surfaced error as `retryAfter` (seconds) for policy layers. Co-authored-by: Cursor <cursoragent@cursor.com>
The live() retry commit accidentally included unrelated, uncommitted quiescence work from a concurrent session (packages/signals core, solid server signals, treeshake test). This restores those files to their prior committed state; the in-progress work returns to the working tree, uncommitted, where it was. Co-authored-by: Cursor <cursoragent@cursor.com>
) Production ids were `<xxhash32(path)>-<ordinal>`, so appending a server function to a file renumbered the rest and clients holding the old numbering silently dispatched to different code with a 200. Ids are now `<name>-<xxhash32(root-relative path)>`, with a trailing ordinal only when the same descriptive name recurs in one file: appends, deletes, reorders, and body edits move no address, and a removed or renamed function becomes a clean 404 instead of a wrong call. Development and production now share the exact same id format. Frozen directive fixtures regenerated deliberately per their README; the diff is id literals only. Co-authored-by: Cursor <cursoragent@cursor.com>
…3110) A call whose well-formed address is not registered in the answering deployment is the ordinary consequence of deploying under open tabs, and until now it surfaced as a bare 404 nothing could act on. The server now labels it with X-Server-Function-Unknown, and the client stamps unknownFunction: true (plus a directed message) on the rejection, so an integration can recover — reload the document onto the current build — instead of showing a generic failed call. Meaningless-path 404s stay unlabelled: a mistyped route is not skew. Co-authored-by: Cursor <cursoragent@cursor.com>
…hims (#3102, #3107) Scripted callers now receive masked redirects as X-Server-Function-Redirect: <status> <url>, the target resolved server-side against the request url — the meaning HTTP assigns the Location a form post would have received. Location never rides a masked 200 (no HTTP meaning there, and it collided with authored Locations on forwarding statuses like a 201's created-at), and integrations compare origins on a real url instead of guessing navigation strategy from the author's spelling — the #3107 relative/absolute coin toss removed at the source. decodeRedirectHeaderValue is exported for readers; a real 3xx reaching the transport (a peer that opted out of following) still passes through whole. Also retires the RC transition shims per the breaking-address release: the instance-header scripted fallback at the bare address and its forced no-store (#3094) are gone — the answer shape is a function of the url alone. Co-authored-by: Cursor <cursoragent@cursor.com>
A derived optimistic store's source draft composed the caller's optimistic overlay in post-await continuations: a generator's store.push read length through an action's optimistic row and landed truth at the wrong index, permanently committing [null, row]. Serve-side overlay gates (values, length, membership, keys, descriptors) now also admit the authoritative-write posture — the same pair ensurePB already used to seed authoritative drafts — so truth authors never read tentative state. User setter drafts keep composing on the optimistic view (#2951); write-side machinery (patch emission, tentative rebasing) keeps composing explicitly. Co-authored-by: Cursor <cursoragent@cursor.com>
Awaiting refresh(x) gives imperative flows and action steps the settle point without a reactive read: accessor targets deliver the settled value, store targets deliver the node passed, failures reject at the yield, and supersession folds every waiter onto whatever finally lands. Staged landings under a held transaction deliver (resolve/until parity, #2930) and never the caller's own optimistic override; bare refresh stays verdict-quiet and fire-and-forget callers never see an unhandled rejection. The waiter is resolve()'s effect machinery plus one reader bit (CONFIG_FRESH_READ) that pulls a still-dirty source through recompute inline, deferred a microtask so same-tick refreshes still coalesce into one re-ask. Costs 694B min / 256B gz to refresh importers only; the core floor stays under its guard, paid by nullish golfs. updateIfNecessary now also refuses disposed nodes (#2983's class). Co-authored-by: Cursor <cursoragent@cursor.com>
The scripted transport sends no conditional headers, so a hand-rolled
respond(undefined, { status: 304 }) answers a question the caller never
asked: the bodiless answer resolves the call to undefined and consumer
state clears, reading as data loss. The 304 still forwards untouched —
it is real HTTP for unscripted callers and the natural conditional-read
answer — but the dev build now names the function and points at the
supported shape: GET-declared reads with ETag/Cache-Control, where the
browser owns the conditional exchange and replays its cached 200.
Also scrubs RFC 10's mention of the retired instance-header transitional
courtesy.
Co-authored-by: Cursor <cursoragent@cursor.com>
…cy raw payload The lone-unnamed-fold special case existed to stay byte-identical with peers predating named sources; with the RC already breaking the wire, it leaves with the other shims. The envelope is always keyed by source id — the unnamed registration rides under its reserved id "true" like any other — and the client always delivers data[source] to each consumer. The unrecognized-opt-in courtesy goes with it: an id naming no registered hook folds nothing, instead of falling back to the unnamed hook. Co-authored-by: Cursor <cursoragent@cursor.com>
The capture runs inside the effect's own compute, where the ambient owner IS the effect — a Computed, which is exactly what dispose() takes. Typing it Owner failed declaration emit (TS2345) and took Solid CI, Size, and CodSpeed down with it. Type-only; no runtime change. Co-authored-by: Cursor <cursoragent@cursor.com>
The #3108 truth-author authoritative-read fix (88fa9d6) and the refresh() quiescence promise (51ffcb9) land on always-retained optimistic/settle-walk paths, so the signals-carrying scenarios drifted 9-69 B past their caps (isPending/latest +16 B, simple-app floor +13 B, full-store hydration +69 B, patchDriver flip +9 B). The web/frames scenario is untouched. Measured locally at HEAD; each bump keeps the usual headroom and is annotated in the config. Co-authored-by: Cursor <cursoragent@cursor.com>
…ough — no merge (#3105) Merging one source is pure overhead, and the mergeProps memo consumed a hydration id the SSR fast path never allocated, so every element after a reactive lone spread went unclaimed. Both DOM generates now hand the accessor straight to spread(), which resolves a function props source inside its own tracking scopes (the Solid 1 shape). Server output is untouched; the ids agree because neither side mints anything. Supersedes the PR's server-side approach (deferring a merge until after the element key): the client is the side doing unnecessary work. The universal generate keeps its merge: it has no hydration ids to drift, and its condition-memo insulation for arbitrarily expensive custom-renderer props is a documented trade-off. New hydration-parity harness scenario pins consecutive ids across the spread (div _hk=0, button _hk=1) end to end. Co-authored-by: Cursor <cursoragent@cursor.com>
…3111) * test(web): pin three server-function invariants that nothing guards The origin gate, the redirect mask's status set, and the error header's bound each came from a recent fix, and each can be undone today without a test failing. - The gate has six branches; two were exercised. Sec-Fetch-Site is authoritative when present, so same-site and none are refused outright and never reach the trusted-origin matcher — loosening same-site is the natural-looking repair for a broken subdomain deployment. Without Sec-Fetch-Site, Origin decides and then Referer; a matcher that answered true for a non-matching origin would fail open unnoticed, and the Referer branch was unexercised entirely. - The mask covers the statuses fetch follows, but only 302 was tested. Narrowing the set to {302, 303} leaves a scripted caller a real 307 that fetch chases before the transport can read it. - The error header's bound is applied by re-encoding a shrinking slice of the SOURCE; cutting the encoded form instead severs a percent escape and the value stops decoding. One message cannot tell the two apart — a naive slice survives whichever padding lands on an escape boundary — so the test sweeps six. Each was checked by mutating the built runtime: loosening same-site fails 1, narrowing the redirect set fails 3, slicing the encoding fails 5 of the 6 paddings. Tests only; no runtime change, so no changeset. * test(web): drop a duplicated case, correct a comment, cover two matchers Review of the first commit: - The parameterised redirect test repeated 302, which the file already covers as "a returned redirect envelope: masked for scripted, real for unscripted". Narrowed to the four statuses nothing exercised, and the describe now carries its issue reference like its neighbours. - The padding sweep's comment was wrong. Measured against a naive `encode(message).slice(0, LIMIT)`: padding 0 is the ONE case that still decodes, because the ceiling lands on an escape boundary — every other phase breaks. So the sweep was five copies of the same signal plus the one case that proves nothing. Two paddings state the property: the aligned case and a misaligned one. - The matrix left `matchesOrigin`'s function and array branches unexercised, which is precisely the fail-open the commit message warned about. Both now have a negative case. Each checked by mutating the built runtime: loosening same-site, a function matcher that returns true, an array matcher that returns true, and narrowing the redirect set each fail a test. * test(web): match the file's idiom for the two-case parameterisations `.each` appears once in all of packages/web/test; a plain loop reads the same and drifts less. The four-status redirect case keeps it, where the parameter is the point. Also a missing blank line.
…3112) * test(web): mark three open server-function gaps as expected failures Each is verified against `next` and stated as the behaviour that is wanted, so the suite stays green while the gap is open and turns red the day it closes. - A result the codec cannot encode is delivered as `undefined`. The function already ran; only the encoding failed, and it failed after the head was committed, so the status is spent and no error tag can be added. The truncated body decodes to the same answer a void function gives, so a write that succeeded is indistinguishable from one that returned nothing. - A streamed result has no backpressure: the stream is built with no `pull` and no queuing strategy, and every codec node is enqueued as soon as it is parsed. A consumer reading three chunks over 60ms left the producer 3695 items ahead; on a large or infinite stream one slow client buffers the whole result in server memory. - The decode depth cap guards the seroval path only, and the body format is chosen by the caller, so selecting the JSON format opts out of it: depth 5000 decodes where the capped path answers 400. `.fails` rather than the repo's `test.skip` idiom because the point is to notice the fix. Tests only; no runtime change, so no changeset. * test(web): fix the depth gap's header, and make two assertions honest Review of the first commit turned up three things, one of which made a test worthless: - BODY_FORMAT_HEADER is not exported from the server entry, so the import was `undefined` and the request carried a header literally named "undefined". The JSON format was never selected and the function received no argument at all: the handler answered {"depth":0} where the gap needs {"depth":500}. The test failed, but not for its own reason — exactly the failure mode `.fails` cannot show you. Local const, as `server-functions-failure-signal.spec.tsx` already does. - Depth 5000 sat on the repo's own cliff (shared.ts notes ~5900 nested objects overflow V8's default stack on CI). 500 is comfortably past the 64-level cap and nowhere near it. - The backpressure ceiling was wall-clock, and under `.fails` a starved CI that produced fewer than 500 would have turned red for no reason. Counted in event-loop turns instead: bounded stays near the queue size on any machine, unbounded tracks the turn count. Also adopts MATRIX.md's spelling (`test.fails` with a `// GAP:` comment), which is the repo's documented idiom for this and which the first commit wrongly described as absent, and drops a dead assertion after rejects.toThrow(). * test(web): point each gap marker at the issue tracking it MATRIX.md's convention is a `// GAP:` comment; naming the issue in it means whoever closes one finds the test that turns red.
…steps. A ~60-line integration (runEffect + effectAction) shows Solid 2.0 consuming Effect programs natively: superseded memo flights interrupt their fibers via AsyncIterable return(), and a cancellable checkout saga runs Effect steps as action transaction boundaries with typed errors, compensation, and optimistic revert. Includes repro-close-timing.mjs documenting the isPending teardown deferral filed as #3122. Co-authored-by: Cursor <cursoragent@cursor.com>
Nothing bounded what a call could send: a 32 MB body was buffered and decoded before any application code could decline it, and a modest argument list forced a range error out of any function when spread into the call. bodySizeLimit (default 1 MiB) refuses an oversized POST body or ?args= encoding with 413 before decoding — a declared Content-Length is checked up front, a chunked body is buffered under the cap — and maxArguments (default 1000) refuses an oversized argument list with 400. Both configurable server-wide and per handler; Infinity removes a bound. The decode depth cap now holds whichever body format the caller selects (#3119): plain JSON walked into a bare JSON.parse with no ceiling where the framed codec enforced 64 levels. The same ceiling now applies to both, iteratively so the check cannot re-create the overflow it prevents, and a non-array argument encoding answers 400 in either format. The open-gaps marker for #3119 comes off; the test stays as an ordinary guard. Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): sanitize failures that escape through the result graph sanitizeServerError guards the one road a thrown error takes out of dispatch. A failure can also escape through the RESULT GRAPH — a rejected promise, an async iterable that throws, a stream that errors — where it reaches the codec as a value to encode rather than as a throw, and never meets the sanitizer. Same failure, different road, and the leak is the exact one the sanitizer exists to stop: an ORM error's message and own-properties (failing query, connection string, bound params) riding the wire verbatim. Worse than the thrown case, because the head is already committed, so the answer is a 200 carrying no error tag. Claimed as a codec plugin rather than by walking the result: a walk would have to run before serialization, and a rejection has not happened yet at that point. The replacement is branded with markSafeError so the plugin does not claim it again, which also leaves the wire shape a plain Error node — the peer needs no matching plugin and the protocol is unchanged. The sanitizer composes ahead of an app's own plugins: a custom error type reaching the client is intent, and intent is spelled markSafeError. #3095's authoring error is branded for the same reason — it names the status the author got wrong, so it must stay readable. * fix(web): sanitize failures that escape through the result graph sanitizeServerError guards the one road a thrown error takes out of dispatch. A failure can also escape through the RESULT GRAPH — a rejected promise, an async iterable that throws, a stream that errors — where it reaches the codec as a value to encode rather than as a throw, and never meets the sanitizer. The leak is the one the sanitizer exists to stop: a driver error's message and own-properties (failing query, connection string, bound params) riding the wire verbatim, under a 200 carrying no error tag because the head is already committed. The three channels are wrapped before the codec sees them. Not the rejection, which has not happened yet, and not the Errors already in the graph: an Error reached as a value was never thrown, so it is data and the author's to ship. Containers are rebuilt only along paths that actually contain a channel, so a healthy response allocates nothing and reference identity survives for the codec; a WeakMap keeps a repeated reference one object and terminates cycles. A first attempt claimed Error via a codec plugin. That was wrong twice over: a plugin WRAPS rather than replaces, so the wire carried a plugin node the peer had no plugin for, and sanitizeServerError's own unbranded replacement was claimed too — breaking every sanitized error, including the ordinary thrown one. Both are now regression tests. #3095's authoring error is branded markSafeError: it names the status the author got wrong, so it is intentional client-facing content. * fix(web): walk data properties only Reading through a getter invoked it during the walk as well as when the codec encodes it, and a throwing one escaped into dispatch's catch to be reported as the function itself failing — the phantom error over a call that succeeded that encodeResult goes out of its way to avoid. Measured: a result with a throwing getter answered 500 with the guard, 200 without. Descriptors are carried across when a container is rebuilt, so a frozen or non-writable shape survives. A channel behind an accessor is left unguarded: invoking it is not ours to do. * fix(web): guard the frames flight sink too It encodes its outcome with its own serializer (serializeStream, not serializeResponseStream), so the guard never reached it: a rejection nested inside a flight-data slice arrived with its message and own-properties intact, under a 200 with no error tag, on the same build where the plain response path was already sanitized. Reachable whenever the response routes through frames — a mutation whose result is markup — with the failure nested one level inside a slice, which is the ordinary shape of a cache entry. A rejection at the TOP of a slice already threw into dispatch's catch and was sanitized; that is why this looked covered. The spec needs the frames server entry, hence the alias alongside the other subpath aliases in vite.config.server.mjs. * fix(web): make the walk cycle-safe, and cover Map and Set Review found three defects in the first shape of the guard, all now tests: - A cycle recursed until the stack gave out, because the container was recorded in the WeakMap AFTER its children were walked. The RangeError then escaped into dispatch's catch as a 500 — on a shape seroval encodes natively as a back-reference. Containers are now recorded before descending, and a cycle forces the rebuild to stand since a descendant already holds it. - A rejection inside a Map or Set reached the wire raw: neither was walked, and the changeset promised more than the code delivered. - A null-prototype object on a channel path was rebuilt as a plain object, changing its node type from NullConstructor to Object. The prototype carries across now. Also: the ReadableStream branch was dead — a stream is async-iterable on every server runtime, so the iterator branch claimed it first — and it acquired the reader eagerly at walk time. It now runs ahead of the iterator branch and takes the reader on first pull. The markSafeError on #3095's authoring error is dropped: that error is encoded as a value and never routed through sanitizeServerError, so the brand did nothing. Verified by removing it and re-running the spec that asserts the message reaches the client.
When the codec could not encode a result the head was already committed —
status spent, no error tag possible — and the body simply stopped. A
truncated body decodes to undefined, the same answer a void function
gives, so a mutation that ran and committed was indistinguishable from
one that returned nothing, and a data layer might retry it.
The failure now travels in band: a terminal error-trailer frame, a
!-prefixed payload on the existing chunk framing (codec frames always
open with `{`, so the prefix is unambiguous). The decoder throws it — as
the call's failure when it is the first frame, and into every
still-pending async value when a later value's encoding fails mid-stream,
the delivered head keeping its data. Sanitized like any thrown error:
generic in production, cause preserved in dev. A crafted trailer in the
arguments answers 400. Version skew degrades safely — an old client
reading a trailer fails the call with a decode error rather than
resolving undefined.
The open-gaps marker for #3117 comes off; the test stays as an ordinary
guard, with the mid-stream and injection pins alongside.
Co-authored-by: Cursor <cursoragent@cursor.com>
The origin gate is skipped for GET-declared reads by design — same-origin policy already keeps a cross-site caller from READING the response, and the gate's Vary fragments the shared-cache entries the helper exists to enable — but the premise the skip rests on lived nowhere: GET() did not document it and nothing offered an alternative for deployments that do not want the trade. Declaring GET is now documented on both entries as the safety assertion it is (the function becomes executable from any origin, with caller-chosen arguments, carrying the user's ambient cookies — declare only reads that are safe in the RFC 9110 section 9.2.1 sense), and csrf.protectDeclaredReads opts a deployment's reads into the origin gate when shared caches are not in play. Tests pin both halves: the default skip, so nobody "fixes" it into cache poisoning, and the opt-in gate. Co-authored-by: Cursor <cursoragent@cursor.com>
#3120) Three guards for what nothing previously watched: - directives-id-scheme.test.js derives expected ids with an INDEPENDENT implementation — the xxhash32 the retired Babel pass used, plus the documented <name>-<hash>[-<ordinal>] format — and compares against what transformDirectives reports. A change to the hash, the relative-path derivation, or the format turns it red even if the frozen fixtures were regenerated wholesale. - The repeated-names fixture pins the ordinal suffix byte-for-byte, including its assignment order: post-bubble program order (top-level declarations hoist in reverse source order, matching the frozen Babel reference), so makeB's submit takes the bare id. A traversal change that flips this re-points deployed addresses; now it turns a test red first. - The fixtures README states that ids are a wire contract: a regeneration that changes any id is a protocol change to be reviewed as one, never waved through as fixture churn. Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 6eb97fa The changes in this PR will be included in the next version bump. This PR includes no changesetsWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Thank you for taking this on! Unfortunately the branch was cut from |
Summary
fix bug from #3133
flattenArray overwrote the accumulated needsUnwrap flag with the reslut of each nested array. When an accessor was followed by a static nested array, later recursive call returned false, causing flatten to return raw accessor instead of a resolving wrapper. Universal renderers could consequently receive accessor function in insertNode.
Also adds regression coverage for: accessor followed by nested static array and accessor inside a nested array followed by another static fragment.
How did you test this change?
Before applying the fix, both new regression tests failed because flatten returned an array containing the raw accessor instead of a function wrapper.
After applying the fix:
pnpm --filter @solidjs/signals testResult:
Test Files 115 passed (115)
Tests 1431 passed | 3 skipped (1434)
also:
pnpm --filter @solidjs/signals types pnpm exec prettier --check packages/signals/src/boundaries.ts packages/signals/tests/flatten.test.ts .changeset/fix-flatten-nested-accessor.md git diff --checkall completed great.