Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e5f9214
feat(web,solid): unified For driver spike — one structure owns rows a…
ryansolid Sep 2, 2026
bf9144f
perf(web): unified For batch clear — whole-parent N→0 rides one textC…
ryansolid Sep 2, 2026
5640b75
perf(web): unified For row diet — mapArray's owner shape, flatten fas…
ryansolid Sep 3, 2026
fd8af38
perf(web): unified For full-replace fast path — no-survivor windows b…
ryansolid Sep 3, 2026
e15fa23
feat(web): ownerless-rows measurement flag + the create-floor finding
ryansolid Sep 3, 2026
ddefa84
perf(web): unified For lazy structure — flat first fills, materialize…
ryansolid Sep 4, 2026
c633948
refactor(web): unified For slot behind renderer ops — platform handed…
ryansolid Sep 4, 2026
73e9f98
feat(solid,web): unified For default-on — the slot rides For's module…
ryansolid Sep 4, 2026
60dbd01
fix(solid,web): unified For P0 sweep — ownership-safe bulk clears, em…
ryansolid Sep 5, 2026
966acdd
test(web): reconcile parity matrix — slot vs live classic oracle acro…
ryansolid Sep 5, 2026
7f97bd9
feat(solid,web,signals): unified For hydration — slot claims server r…
ryansolid Sep 5, 2026
d3ab430
feat(web,solid): unified For hole seam — lists passed through props.c…
ryansolid Sep 5, 2026
d9d2323
test(web): add @jsxImportSource pragma to unified For specs
ryansolid Sep 5, 2026
3b9caa9
feat(solid,web): unified For hydration — anchored holes engage
ryansolid Sep 5, 2026
029ef37
fix(solid,web): unified For hydration — nested claim recording, synch…
ryansolid Sep 5, 2026
569a17f
chore(size): ratchet after rebase over #3183 and audit round 3
ryansolid Sep 5, 2026
ec0169f
fix(web,solid): unified For hydration — hydrating demote writes the h…
ryansolid Sep 6, 2026
1768e7e
fix(web): unified For direct seam hands classic the bounded region on…
ryansolid Sep 6, 2026
a6e23b5
test(web): pin text-row hydration mismatch — no orphaned or duplicate…
ryansolid Sep 6, 2026
2ece66d
refactor(web): unified For hole demote is synchronous in all modes; r…
ryansolid Sep 6, 2026
ecda2f7
fix(web): unified For hole seam — hand off the range BEFORE the slot'…
ryansolid Sep 7, 2026
8a53177
chore(size): absorb upstream drift after rebase (2026-09-06)
ryansolid Sep 7, 2026
2efb020
docs(solid): unified For placement — why direct insertBefore, not fra…
ryansolid Sep 7, 2026
c6495fd
chore(size): absorb #3296 store-module drift after rebase (2026-09-07)
ryansolid Sep 7, 2026
800edba
fix(solid,web): unified For — external audit round (ownership, dynami…
ryansolid Sep 7, 2026
fe97aa3
feat(solid,web,signals): unified For becomes THE keyed-For engine on …
ryansolid Sep 8, 2026
1c9ed27
fix(solid): engine — flat mode for every keyed mode, detect-only hydr…
ryansolid Sep 8, 2026
4c1fc3e
feat(universal): createRenderer engages the unified For engine with r…
ryansolid Sep 8, 2026
307bb3e
feat(solid): one engine — For's plain call is answered by the engine'…
ryansolid Sep 8, 2026
64a59ec
feat(signals,solid,universal): ONE list engine — mapArray is the engi…
ryansolid Sep 8, 2026
97168b9
fix(signals,solid,web,universal): list engine — #3308 audit round (ow…
ryansolid Sep 8, 2026
ccc1e18
feat(signals): list engine reports list-identity churn to attribution…
ryansolid Sep 8, 2026
19b3caa
test(web): unified For specs type-check under test-types (@solidjs/we…
ryansolid Sep 8, 2026
0dfa12f
test(web): pin the reclaim sweep's real justification — an element sh…
ryansolid Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/unified-for-slot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"solid-js": patch
"@solidjs/web": patch
"@solidjs/signals": patch
"@solidjs/universal": patch
---

Unified For: on web, keyed `<For>` is driven by one persistent engine that owns both row bookkeeping and DOM placement — an intrusive row chain updated by a prefix/suffix walk plus a middle-window pass (mapArray's own matching, duplicates included) inside an ordinary two-phase render effect, with LIS placement at commit — replacing the mapArray + reconcileArrays double pass. Structural operations (swap, reorder, insert, remove) run 1.2–7x faster across jfb and uibench; creation and clear stay at parity via flat-mode first fills for identity-keyed rows (parallel arrays; the chain materializes lazily on the first partial structural op).

**Semantics are mapArray's, unchanged.** The engine implements every `For` mode itself — `keyed` default (reference identity), `keyed={false}` (positional reuse, item accessor + plain index), `keyed={fn}` (key function, item accessor + index accessor), index accessors by row arity, legal duplicates (the second occurrence of a key reuses the second old row, in mapArray's pairing order), `fallback` as an owned empty-state row, array-like subjects — so there is no engage/decline/demote seam and no classic fallback path on web. **There is one list implementation.** The engine lives in `@solidjs/signals` (`list.ts`); `mapArray` _is_ its array output (same API and contract: same values, same array identity while structurally unchanged, `[fallback]` when empty, `_parentComputed` routing for row bodies), and a plain call of a `<For>` accessor (`children()`, introspection, renderers that don't engage) returns the same. `For`'s rendered output drives the engine through a **node layer** — the only code that touches nodes — which solid-js builds over a renderer's `SlotOps`; web (`domOps`) and `@solidjs/universal` (`createRenderer` primitives) both consume it. The pre-engine mapArray is kept only as a test reference: the oracle harness compares the engine against it across every mode, dynamic rows, duplicates, fallback and hydration. Rows live under the `<For>`'s creation owner (context, boundaries, lifetime follow the source position); a row that throws during its build disposes its owner before the error rides the boundary; empty-rendering rows render zero nodes (neighbor-anchored); the list end stays contiguous past foreign trailing nodes; removes only detach nodes still under the list's parent.

Rows whose top level resolves to a function — a component returning `<Show>`/`<Dynamic>`/a conditional, a memo, a fragment with accessor children — are dynamic rows: built once (owned, untracked) and resolved by the engine's own compute, tracked, exactly the `flatten` read classic's insert effect performs for them. No per-row effect, no marker nodes, so user row code never runs twice; a flip splices only that row's range, reusing positional text nodes with a `.data` write; a NotReady thrown from a row's resolution parks the built plan and the retry reuses the rows.

Delivery is zero-API and zero-compiler: `For` stamps a `$for` descriptor on its accessor, the engine rides `For`'s module graph, and web's `insert` engages it with a renderer-ops singleton (`domOps`); the engine itself is platform-free (opaque `SlotNode`, every node touch through `SlotOps`). `@solidjs/universal`'s `createRenderer` engages it too, with ops built from the renderer's own primitives (`insertNode`, `removeNode`, `getNextSibling`, `replaceText`, …; nodes are non-array objects, and text data is tracked for engine-created text nodes) — no new renderer options. A `For` passed through a component's `{props.children}` engages on both platforms.

Hydration: lists engage during hydration and claim the server rows themselves (whole-parent and comment-bounded holes), minting the same ids classic's mapArray owner would (`For` peeks the id via an `enableHydration()`-installed hook; mapArray gains an internal `lazy` option). Primitive rows adopt the server's positional text nodes; the fill commit reconciles against the region only on server/client mismatch and reports the repair once in dev. Nothing can demote mid-fill, so claims are never handed back. Hydration code lives in a module installed by `enableHydration()` — CSR bundles shake it.

Internal: `@solidjs/signals` exports the engine (`createListEngine`, `listArray`, the node-layer types) marked `@internal` for solid-js.
24 changes: 24 additions & 0 deletions packages/signals/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,30 @@ export type {
} from "./signals.js";
export { affects } from "./affects.js";
export { mapArray, repeat, type Maybe } from "./map.js";
/** @internal The list engine behind mapArray and <For>: solid-js builds a
* node layer over its renderer ops and drives the RENDERED output; the
* ARRAY output is mapArray itself. */
export {
createListEngine,
listArray,
IDENTICAL as LIST_IDENTICAL,
firstOf,
lastOf,
nodesOf,
firstNodeOf,
lastNodeOf,
firstNodeFrom,
type ListEngine,
type ListMeta,
type ListNodeLayer,
type ListSlot,
type ListRow,
type ListFlatPlan,
type ListPlan,
type ListNode,
type Nodes as ListNodes,
type Leaves as ListLeaves
} from "./list.js";
export * from "./store/index.js";
export {
createLoadingBoundary,
Expand Down
Loading
Loading