Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion ref/SOLID_NEXT
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0847122e9fc9681fda7feef37d5427db7316e56d
d2323b077c7dda32ebced7843c2ce2b788a91e8d
5 changes: 4 additions & 1 deletion ref/compile/rt-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { provideRequestEvent } from "@solidjs/web/storage";
import { Show, For, Loading, Errored, createComponent, createSignal, createMemo, createEffect, createOptimistic, createOptimisticStore, createStore, createProjection, createDeferred, untrack, latest, isPending, NotReadyError, getOwner, runWithOwner, getNextChildId, runInServerComponentScope } from "solid-js";
import { escape, renderToStream, markSafeError, generateHydrationScript } from "@solidjs/web";

// 05 H-9 / Solid D-24: `isPending(fn)` judges the reads fn makes, never its return value, so `pending? x` over a structured
// value (a store, a keyed projection) reads its keys — Solid's "whole-store questions are asked through reads"
const readWhole = (v: unknown) => { if (v !== null && typeof v === "object") Reflect.ownKeys(v); return v; };
export type Acc = () => unknown;
export type Writer = { set: (v: unknown) => void; path: (keys: (string | number)[], v: unknown) => void; guess: (v: unknown, keys?: (string | number)[]) => void };
export type Entry = { get?: Acc; w?: Writer; value?: unknown };
Expand Down Expand Up @@ -207,7 +210,7 @@ export function createApp(opts: { api?: Record<string, (...a: any[]) => unknown>
isA, PANIC, catchMemo: catchAcc, tryCatch, catchRead: (f: () => unknown, match: (err: unknown) => boolean, alt: (err: unknown) => unknown) => catchAcc(f, match, alt)(),
throw: (e: unknown): never => { throw e; },
eq: (a: unknown, b: unknown) => a === b,
untrack, latest: (f: () => unknown) => latest(f as any), isPending: (f: () => unknown) => isPending(f as any),
untrack, latest: (f: () => unknown) => latest(f as any), isPending: (f: () => unknown) => isPending(() => readWhole(f())),
fmt,

// ---- views: compiled JSX (compile/jsx.ts, generate "ssr", hydratable) does the rest ----
Expand Down
5 changes: 4 additions & 1 deletion ref/compile/rt-web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
import { getContext, setContext, $TARGET } from "@solidjs/signals";
import { hydrate, render } from "@solidjs/web";

// 05 H-9 / Solid D-24: `isPending(fn)` judges the reads fn makes, never its return value, so `pending? x` over a structured
// value (a store, a keyed projection) reads its keys — Solid's "whole-store questions are asked through reads"
const readWhole = (v: unknown) => { if (v !== null && typeof v === "object") Reflect.ownKeys(v); return v; };
export { flush };
export type Acc = () => unknown;
export type Writer = { set: (v: unknown) => void; path: (keys: (string | number)[], v: unknown) => void; guess: (v: unknown, keys?: (string | number)[]) => void };
Expand Down Expand Up @@ -259,7 +262,7 @@ export function createApp(opts: { root: any; api?: Record<string, (...a: any[])
isA, PANIC, catchMemo: catchAcc, tryCatch, catchRead: (f: () => unknown, match: (err: unknown) => boolean, alt: (err: unknown) => unknown) => catchAcc(f, match, alt)(),
throw: (e: unknown): never => { throw e; },
eq: (a: unknown, b: unknown) => raw(a) === raw(b),
untrack, latest: (f: () => unknown) => latest(f as any), isPending: (f: () => unknown) => isPending(f as any),
untrack, latest: (f: () => unknown) => latest(f as any), isPending: (f: () => unknown) => isPending(() => readWhole(f())),
fmt,

// ---- views: compiled JSX (compile/jsx.ts, generate "dom") does the rest ----
Expand Down
5 changes: 4 additions & 1 deletion ref/compile/rt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
flush, untrack, latest, isPending, action, refresh, affects, until, NotReadyError, createContext, getContext, setContext, getOwner, runWithOwner, $TARGET, createErrorBoundary,
} from "@solidjs/signals";

// 05 H-9 / Solid D-24: `isPending(fn)` judges the reads fn makes, never its return value, so `pending? x` over a structured
// value (a store, a keyed projection) reads its keys — Solid's "whole-store questions are asked through reads"
const readWhole = (v: unknown) => { if (v !== null && typeof v === "object") Reflect.ownKeys(v); return v; };
export { flush };
export type Acc = () => unknown;
export type Writer = { set: (v: unknown) => void; path: (keys: (string | number)[], v: unknown) => void; guess: (v: unknown, keys?: (string | number)[]) => void };
Expand Down Expand Up @@ -266,7 +269,7 @@ export function createApp(opts: { api?: Record<string, (...a: any[]) => unknown>
errorClass, isA, PANIC, catchMemo: catchAcc, tryCatch, catchRead: (f: () => unknown, match: (err: unknown) => boolean, alt: (err: unknown) => unknown) => catchAcc(f, match, alt)(),
throw: (e: unknown): never => { throw e; },
eq: (a: unknown, b: unknown) => raw(a) === raw(b),
untrack, latest: (f: () => unknown) => latest(f as any), isPending: (f: () => unknown) => isPending(f as any),
untrack, latest: (f: () => unknown) => latest(f as any), isPending: (f: () => unknown) => isPending(() => readWhole(f())),
fmt,

// ---- views: compiled JSX (compile/jsx.ts) calls the renderer; these are the two helpers it calls on __rt ----
Expand Down
2 changes: 1 addition & 1 deletion ref/oracle/lower.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ export class Oracle {
const v = this.ev(e.e, scope, env); return !e.stream && v instanceof Flight ? v.first : v; // in tail position the memo returns the flight (R-1): a stream for `await*`, else its one answer
} // in tail position the memo returns the flight (R-1); elsewhere the operand checks below refuse it
case "latest": { const was = this.imperative; this.imperative = 0; try { return latest(() => this.ev(e.e, scope, env)); } finally { this.imperative = was; } } // R-12: `latest` of a value that never had one throws, in a handler as anywhere (Solid S-28)
case "pending": return isPending(() => this.ev(e.e, scope, env));
case "pending": return isPending(() => { const v = this.ev(e.e, scope, env); if (v !== null && typeof v === "object") Reflect.ownKeys(v); return v; }); // Solid D-24: the probe judges reads, so a structured value is read whole
case "if": return this.ev(e.c, scope, env) ? this.ev(e.t, scope, env) : this.ev(e.f, scope, env);
case "throw": throw this.ev(e.e, scope, env); // R-f-3
case "serverfn": { let f = this.serverFns.get(e); if (!f) this.serverFns.set(e, (f = this.serverFn({ name: this.serverNames.get(e) ?? "server fn", params: e.params, body: e.body }, scope))); return f; } // SF-1d
Expand Down
20 changes: 15 additions & 5 deletions ref/oracle/repros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ re-runs and recovers when the content does (S-fail-1 instants 6–8).

---

## DV-8 — a keyed projection over a memo stops following it after its first value (mostly fixed on `next` by solidjs/solid#4280; filed as #4271 — what remains is in 05's DV-8 record)
## DV-8 — a keyed projection over a memo stops following it after its first value (fixed on `next` by solidjs/solid#4280; filed as #4271 — the two cases left in S-ai2-search were our lowering, `dv8b-projection-pending.mjs`)

Found by an agent-written app (S-ai2-search): `results key id = q.length < 2
? throw TooShort(q) : await search(q)`, lowered as
Expand All @@ -145,7 +145,7 @@ in Solid; with `key`, the interpreter does too.

---

## DV-9 — a failed boundary renders no fallback on the server (a Solid bug, ruled 2026-10-26; solidjs/solid#4287)
## DV-9 — a failed boundary renders no fallback on the server (a Solid bug, ruled 2026-10-26; solidjs/solid#4287, fixed on `next`)

**Rule.** 10-server SV-6: a boundary that takes a failure during the server
render is streamed with its fallback drawn, so the browser shows the
Expand Down Expand Up @@ -183,7 +183,7 @@ hydrated "loading"

---

## DV-10 — a held lane holds per render effect (reversed 2026-10-26: Solid's position adopted; solidjs/solid#4284, open)
## DV-10 — a held lane holds per render effect (reversed 2026-10-26: Solid's position adopted; solidjs/solid#4284, closed; the `one` mode is fixed on `next` at `2c9fdd9`)

**Rule.** 05 H-7 (since 2026-10-26, Solid's position): a tracked `latest x`
of an optimistic guess reads ahead of the guess's held lane; the lane's
Expand Down Expand Up @@ -281,7 +281,7 @@ between two replayed handlers, so both actions read the committed `n`.

---

## DV-15 — a client question's served fallback is rebuilt on hydration (a Solid bug, ruled 2026-10-26; solidjs/solid#4286)
## DV-15 — a client question's served fallback is rebuilt on hydration (a Solid bug, ruled 2026-10-26; solidjs/solid#4286, fixed on `next`)

**Rule.** 10 SV-3a: hydration adopts what the server sent; SV-7b: a client
question's reader shows its hold's fallback on the server, and the browser
Expand Down Expand Up @@ -355,7 +355,7 @@ S-solid-5.

---

## DV-18 — a keyed projection's row readers miss every landing after the first (a regression from solidjs/solid#4280) — filed as solidjs/solid#4305
## DV-18 — a keyed projection's row readers miss every landing after the first (a regression from solidjs/solid#4280) — filed as solidjs/solid#4305, fixed on `next`

**Rule.** RULES-DERIVED D-30, spec ST-4 and H-2: each landing reconciles a
keyed projection's rows, and its readers show it.
Expand All @@ -369,3 +369,13 @@ rows still read `"apple,apricot"` (expected `"apple,applesauce,apple pie"`).
`direct` (one render effect reading `row.name`) follows both. Bisected:
fine at `f811097`, broken at `1212b77` (#4280) and on `2f7dc84`.

---

## DV-8b — `pending?` over a keyed projection (ours, not Solid's)

`dv8b-projection-pending.mjs [proxy|keys|length] [boundary|none]`. The probe
`isPending(() => rows)` reads nothing — Solid judges the reads a probe's
thunk makes, never its return value (RULES-DERIVED D-24, A23) — so it is
`false` while the memo under the projection re-asks. `keys` (what
`__rt.isPending` now does for a structured value) and `length` report
`searching` as S-ai2-search's instants 2 and 4 expect.
23 changes: 23 additions & 0 deletions ref/oracle/repros/dv8b-projection-pending.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// DV-8 (what #4280 left): `isPending` over a keyed projection, read in its own render effect beside a boundary that shows
// the projection, misses its memo's re-ask. Expected (RULES-DERIVED D-30): true while the memo re-asks.
// node --conditions=browser --conditions=development repros/dv8b-projection-pending.mjs [proxy|length] [boundary|none]
import { createRoot, createSignal, createMemo, createProjection, createRenderEffect, createErrorBoundary, createLoadingBoundary, isPending, flush, untrack } from "@solidjs/signals";
const [probe, where] = [process.argv[2] ?? "proxy", process.argv[3] ?? "boundary"];
const [q, setQ] = createSignal("");
const res = {}; let probeShown = "", content = "";
createRoot(() => {
const src = createMemo(() => { const v = q(); if (v === "") return []; if (v.length < 2) throw new Error("too short"); return new Promise(r => (res[v] = r)); });
const rows = createProjection(() => src(), [], { key: "id" });
createRenderEffect(() => isPending(() => probe === "proxy" ? rows : probe === "keys" ? Reflect.ownKeys(rows) : rows.length) ? "searching" : "-", v => { probeShown = v; });
const view = () => `${rows.length} ${rows.map(r => r.name).join(",")}`;
const out = where === "boundary" ? untrack(() => createErrorBoundary(() => createLoadingBoundary(view, () => "loading")(), e => "error:" + e().message)) : () => { try { return view(); } catch (e) { return e?.message ?? "pending"; } };
createRenderEffect(() => out(), v => { content = v; });
});
const tick = async () => { for (let i = 0; i < 3; i++) { await new Promise(r => setTimeout(r, 0)); flush(); } };
const show = (l, want) => console.log(l.padEnd(14), probeShown.padEnd(10), JSON.stringify(content), want ? `(probe expected ${want})` : "");
flush(); await tick(); show("mount");
setQ("a"); flush(); await tick(); show("q = a (throws)", "-");
setQ("ap"); flush(); await tick(); show("q = ap", "searching");
res.ap([{ id: 1, name: "apple" }, { id: 2, name: "apricot" }]); await tick(); show("ap lands", "-");
setQ("app"); flush(); await tick(); show("q = app", "searching");
res.app([{ id: 1, name: "apple" }, { id: 3, name: "applesauce" }]); await tick(); show("app lands", "-");
28 changes: 14 additions & 14 deletions ref/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ref/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"test": "node --experimental-strip-types run.ts",
"oracle": "node --experimental-strip-types --conditions=browser --conditions=development oracle/run.ts",
"compiled": "node --experimental-strip-types --conditions=browser --conditions=development compile/run.ts",
"ssr": "node --experimental-strip-types --conditions=development ssr/run.ts",
"ssr": "node --experimental-strip-types ssr/run.ts",
"types": "node --experimental-strip-types types/run.ts",
"crate": "node --experimental-strip-types crate.ts",
"model": "node --experimental-strip-types model.ts"
Expand Down
12 changes: 0 additions & 12 deletions scenarios/S-ai2-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,3 @@ file: ../examples/ai2/search/main.rx
- 9: The rejection is an `Error`. The armed `loading` shows content, the inner `NotFound` boundary passes `Error` up (R-f-4), and the outer `fallback(e)` shows the message and the retry button. The list is untouched.
- 10: `retry()` is `refresh item`: a quiet re-ask under the same key (AC-4). The error stays the display through the retry (H-12), and readers behind the fallback hold nothing, so the frame is unchanged.
- 11: A value replaces the error, so the outer boundary leaves its fallback and the item shows (H-12). Whether the retry action's change was still open (AC-4: staged into it) or not, it commits with this landing.

## Divergences

- oracle "Frames" instant 2: `too short: a pick a result` — DV-8, not fully fixed by solidjs/solid#4280: after the projection's memo has failed, its re-ask reports no `pending?` through the projection (a plain memo does: no `key` shows `searching…`).
- oracle "Frames" instant 4: `2 results for app apple apricot pick a result` — DV-8: no `searching…`, the projection reports no pending verdict for its memo's re-ask.
- oracle "Frames" instant 5: `2 results for app apple apricot pick a result` — DV-18: since solidjs/solid#4280, a keyed projection's row readers miss every landing after the first, so the list and the count keep the `ap` rows (`ref/oracle/repros/dv18-projection-relanding.mjs`)
- oracle "Frames" instant 6: `2 results for app apple apricot loading…` — DV-18
- oracle "Frames" instant 7: `2 results for app apple apricot item 4 not found` — DV-18
- oracle "Frames" instant 8: `2 results for app apple apricot loading…` — DV-18
- oracle "Frames" instant 9: `2 results for app apple apricot failed: timeout retry` — DV-18
- oracle "Frames" instant 10: `2 results for app apple apricot failed: timeout retry` — DV-18
- oracle "Frames" instant 11: `2 results for app apple apricot applesauce stewed apples` — DV-18
5 changes: 0 additions & 5 deletions scenarios/S-ai2-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,3 @@ Why, one line per instant:
- **9.** A plain local write changes the `density` provision, so both Panels and the Preview update. Nothing else moves.
- **10.** A new key (the density differs) means a new flight. The `settings` guess shows now. The `saveError` guess `""` closes the "Save failed" branch now (H-11; a lane condition's branch change attaches immediately, H-7). `pending? settings` is true.
- **11.** The answer is written as the truth (`settings := saved`), equal to the guess, so it confirms silently. The plain `saveError := ""` is the truth for that source too, so it does not revert to "quota exceeded". The change commits, and `pending? settings` is false.

## Divergences

- server "Frames" instant 1: `` — DV-9: Solid streams an empty fragment for a boundary that fails after the shell; the fallback appears at hydration

5 changes: 0 additions & 5 deletions scenarios/S-hold-5.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,3 @@ Instant 1: the rejection ends the wait like a value (R-6) and reaches the
question; the reader behind the error fallback holds nothing (H-4), so the
instant commits and the fallback stays (Solid D-39). Instant 3: the answer
replaces the error; content returns.

## Divergences

- server "Frames" instant 1: `` — DV-9: Solid streams an empty fragment for a boundary that fails after the shell; the fallback appears at hydration

7 changes: 0 additions & 7 deletions scenarios/S-server-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,3 @@ generic error in its place, and the boundary shows that — the message
never leaves the server. A second request for the page, rendered at the same
time, has its own `profile.rx`: these landings are not its landings, and
its stream stays at the shell.

## Divergences

- server "Server frames" instant 1: `Profile loading posts` — DV-9: Solid streams an empty fragment for a boundary that fails after the shell; the client draws the fallback at hydration
- server "Server frames" instant 2: `Profile` — DV-9
- server "Frames" instant 1: `Profile loading posts` — DV-9
- server "Frames" instant 2: `Profile` — DV-9
6 changes: 0 additions & 6 deletions scenarios/S-server-3.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,6 @@ and the hydrated page: the browser draws its answers as ordinary landings.
Served, the stream is the mount alone (`zone UTC measuring`): the landings
are the browser's, after it has taken the page over.

## Divergences

- hydrated "Frames" instant 0: `zone UTC measuring` — DV-15: hydrating,
Solid drops the served `<p>measuring</p>` and builds its own (the same
text); the rule adopts it (SV-3a). The frames after are the rule's.

## Rejected variants

```
Expand Down
5 changes: 0 additions & 5 deletions scenarios/S-solid-38.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,3 @@ export default component App() {
Instant 2: the write publishes beside the old error; the inner `loading` is
hidden behind it — a pending is not a value, and the error fallback never
clears to show broken state underneath.

## Divergences

- server "Frames" instant 1: `0` — DV-9: Solid streams an empty fragment for a boundary that fails after the shell; the fallback appears at hydration

Loading