solid2-agent-kit 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,7 +41,8 @@ Practical consequences:
41
41
  | Expensive derivation, multiple consumers, or equality boundary needed | `createMemo(() => ...)` |
42
42
  | Follows a prop but user can locally override; prop change resets | writable derivation: `createSignal(() => props.value)` / `createStore(() => props.value, fallback)` |
43
43
  | Shared by a subtree, independent per provider instance | signal/store created inside a provider component, passed via context |
44
- | Truly app-wide singleton (theme, session, locale) | module-level signal/store (beware SSR: module state is shared across requests) |
44
+ | App-wide state (theme, session, cart, locale) | the same provider, placed at the root of `App` — not a module-level signal/store (under SSR one module instance serves every request) |
45
+ | Constants (a category list, a formatter, the `createContext` call) | module scope |
45
46
  | Tentative value during a mutation | `createOptimistic` / `createOptimisticStore` + `action` |
46
47
  | Values pushed by an external subscription (websocket, reactive client) | async iterable returned from a memo (or function-form `createStore` for keyed reconciliation) — never `{ data, error }` signal pairs |
47
48
 
@@ -72,9 +73,9 @@ Most React `useEffect` code should NOT become `createEffect`:
72
73
  | Overlay / modal | `<Portal>` — hoist async reads *above* the portal (reads inside start on the client) |
73
74
  | Async value used several layers down | Create the memo high; pass `value={memo()}` through intermediates (they do not wait); put `<Loading>` around the leaf read |
74
75
  | Nested child with its own fetch | Leave it nested — it runs in parallel. Sequential only when the second call needs the first response (`fetchAuthor(story().authorId)`) |
75
- | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render) |
76
+ | Browser-only widget (charts, maps, `window`) | `clientOnly(() => import("./Chart"))` from `@solidjs/web` (`{ lazy: true }` defers the import until first render; `{ export: "Chart" }` for a named export). Placeholder: `<Chart fallback={<Skeleton />} />` — SSR renders the fallback; a `<Loading>` around it leaves the server HTML empty |
76
77
  | Browser-only *value* (`localStorage`, viewport size) | `createMemo(..., { ssrSource: "client" })` — per-value policy at the data source, no component split (`"hybrid"` when server data mixes with client signals) |
77
- | Server vs browser branch | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` |
78
+ | Server vs browser code path | `isServer` / `isDev` from `@solidjs/web` (build-time constants), not `typeof window` — never to choose markup (`{isServer ? <A /> : <B />}` is a hydration mismatch; use `clientOnly` / `ssrSource`) |
78
79
  | SSR-stable `id` / `for` / `aria-*` pairing | `createUniqueId()` |
79
80
 
80
81
  ## Canonical patterns
@@ -158,6 +159,32 @@ at component-body top level is a write in an owned scope — dev throws
158
159
  `[REACTIVE_WRITE_IN_OWNED_SCOPE]` (rc.9 closed the exemption store writes had) — so
159
160
  seed the shape through the `createStore` argument or a derivation.
160
161
 
162
+ **The store itself is read-only; only the draft writes.** An assignment or mutating
163
+ call on the store proxy outside a setter is *ignored* — no throw, no dev warning, the
164
+ value does not change, and TypeScript accepts it (`Store<T>` is `T`). This is the
165
+ MobX / Vue / Solid 1 `createMutable` reflex:
166
+
167
+ ```tsx
168
+ // WRONG — all silently ignored
169
+ todos.push(todo);
170
+ state.count++;
171
+ todo.done = !todo.done;
172
+
173
+ // CORRECT
174
+ setTodos((draft) => { draft.push(todo); });
175
+ setState((draft) => { draft.count++; });
176
+ setTodos((draft) => {
177
+ const row = draft.find((t) => t.id === id);
178
+ if (row) row.done = !row.done;
179
+ });
180
+ ```
181
+
182
+ Plain objects and arrays are proxied; `Map`, `Set`, and `Date` are stored as they are.
183
+ `draft.selected.add(id)` inside a setter mutates the `Set` but notifies nobody. Keep a
184
+ set of ids as `Record<string, true>` (`draft.selected[id] = true` /
185
+ `delete draft.selected[id]`, both tracked) or an array, or assign a new instance
186
+ (`draft.tags = new Set([...draft.tags, tag])`) so the property itself changes.
187
+
161
188
  **External collections enter stores through reconciliation, never wholesale assignment.**
162
189
  `draft.todos = serverTodos` renders correctly but replaces every object identity — all
163
190
  subscribers under the path re-run and row identity is lost. In order of preference:
@@ -168,7 +195,10 @@ subscribers under the path re-run and row identity is lost. In order of preferen
168
195
  `setState((draft) => { reconcile(fresh, 'id')(draft.todos); })`. Positional data
169
196
  (fixed-shape dashboards): `reconcile(next, null)`.
170
197
 
171
- Plain non-reactive copy for logging/serialization/`structuredClone`: `snapshot(store)`.
198
+ Plain non-reactive view for logging/serialization: `snapshot(store)`. It is not a copy —
199
+ subtrees with no pending changes are the store's own objects, so mutating the snapshot
200
+ (or letting a library `sort()` it) changes the store with no notification. Edit
201
+ `structuredClone(snapshot(store))` instead.
172
202
  Subscribe an effect compute to every nested change: `deep(store)`.
173
203
 
174
204
  ### Children: pass through, or resolve with `children()`
@@ -248,6 +278,25 @@ createEffect(
248
278
  );
249
279
  ```
250
280
 
281
+ `apply` returns a cleanup function or nothing — write it with a **block body**. An
282
+ expression-bodied arrow returns whatever the expression evaluates to: an assignment
283
+ (`(t) => (document.title = t)`) returns the string, and a setter (`(v) => setDraft(v)`)
284
+ returns the value it set. Dev throws `effect callback returned an invalid cleanup value`,
285
+ production throws a `TypeError` on the next run, and both halt the whole
286
+ reactive system (`[REACTIVITY_HALTED]`). TypeScript reports it as TS2345; a JS project
287
+ gets no warning. The cleanup of an effect is the `apply` return value — `onCleanup`
288
+ inside `apply` has no owner and never runs (`[NO_OWNER_CLEANUP]`).
289
+
290
+ ```tsx
291
+ // WRONG — returns the assigned string; halts reactivity (dev: first run, prod: next run)
292
+ createEffect(() => title(), (t) => (document.title = t));
293
+
294
+ // CORRECT
295
+ createEffect(() => title(), (t) => {
296
+ document.title = t;
297
+ });
298
+ ```
299
+
251
300
  Reads inside `apply` do not track. Extract every needed reactive value in `compute`
252
301
  (e.g. `() => ({ name: user.name, role: user.role })`). Passing a store proxy into `apply`
253
302
  and reading `user.name` there *runs once* and never retriggers. Compute-phase errors can be
@@ -454,7 +503,10 @@ registers this hook must load before the server graph — put it in the plugin's
454
503
  `start: { instrument: "./src/instrument.ts" }`, not at the top of an entry (see
455
504
  [Production observability](#production-observability-the-observe-build)).
456
505
 
457
- `render` / `hydrate` accept a per-root `onError` that wins over the ambient hook.
506
+ `render` / `hydrate` accept a per-root `onError` that wins over the ambient hook —
507
+ `render(() => <App />, root, undefined, { onError })` (options are the 4th argument; a
508
+ 3rd-argument object is taken as `init` and the hook is silently not installed) and
509
+ `hydrate(() => <App />, root, { onError })`.
458
510
  Uncaught errors (`REACTIVITY_HALTED`) go to the platform's `reportError`, which every
459
511
  monitor already listens on — do not report them twice. `renderToStream` /
460
512
  `renderToString`'s `onError` option *is* the per-render server hook: it now hears every
@@ -572,12 +624,28 @@ function createTodos() { // call from a component — not module scope (SSR shar
572
624
  Three layers, in this order: durable source → ephemeral UI that must survive
573
625
  overlay discard (the `errors` map, folded in the projection) → optimistic overlay
574
626
  (`pending`, the predicted `completed`). Consumers read one store. Catch
575
- *expected* mutation failures in the action so a toggle error does not blank the
576
- list through `<Errored>`; unhandled projection/render errors still reach the
577
- boundary.
627
+ *expected* mutation failures in the action, because nothing else will show them:
628
+ an error that escapes an action discards its optimistic writes and rejects the
629
+ promise the call returned — it never reaches `<Errored>`, which catches failing
630
+ reads and renders only (projection and render errors still reach the boundary).
631
+ Uncaught, the UI silently reverts and the rejection is unhandled. Put expected
632
+ failures in the errors map or a toast inside the action, and `.catch` the call in
633
+ the handler for the rest:
634
+
635
+ ```tsx
636
+ <button
637
+ type="button"
638
+ onClick={() => {
639
+ removeTodo(todo.id).catch(() => showToast('Could not delete'));
640
+ }}
641
+ >
642
+ Delete
643
+ </button>
644
+ ```
578
645
 
579
646
  Going from a client store to the server is **additive**: same `setTodos`
580
- calls, wrap each mutation in `action`, swap in `createOptimisticStore` and a
647
+ calls, wrap each mutation in `action`, swap in the **function form**
648
+ `createOptimisticStore(() => api.list(), [])` (with `refresh` after the `yield`) and a
581
649
  file of server functions. Do not rewrite `App.tsx` with loading/error branches,
582
650
  and do not snapshot-the-cache / write-a-prediction / restore-on-error. A cache
583
651
  library is not the productionizing step — the sync mutation already was the
@@ -594,6 +662,16 @@ Ordinary signal/store writes inside an action are held until it settles. Never c
594
662
  Do not set a `submitted` signal and watch it from an effect — that work belongs in the
595
663
  handler or the `action`.
596
664
 
665
+ **The value form has no durable layer.** `createOptimisticStore([])` (or
666
+ `createOptimistic(value)`) is a pure overlay: writes inside an action vanish when it
667
+ settles, and writes outside an action do not stick. Copying the reference example
668
+ `createOptimisticStore<Todo[]>([])` and writing the saved row after the `yield`
669
+ leaves an empty list once the action settles, with no warning. Durable data comes
670
+ from the function form — `createOptimisticStore(() => api.list(), [])` refreshed
671
+ after the `yield`, or `createOptimisticStore(() => base.todos, [])` over a separate
672
+ `createStore` that the action writes the confirmed value into. Keep the value form for
673
+ in-flight flags.
674
+
597
675
  `createOptimistic` / `createOptimisticStore` are for a tentative value during an
598
676
  active mutation, not a local editing session (use a writable derivation or a plain
599
677
  store for that). A whole-form `createOptimistic(false)` saving flag is fine; prefer
@@ -772,9 +850,47 @@ function ThemeButton() {
772
850
 
773
851
  Pass accessors/setters/stores through context — never `value={theme()}` (a dead snapshot).
774
852
  No `useMemo` for the value object, no context splitting: the object is created once and
775
- fine-grained updates flow through the signals inside it. Context is **subtree scoping**.
776
- A true app-wide singleton (theme, session, locale) is a module-level signal/store —
777
- do not invent a root provider for that. Module state is shared across SSR requests.
853
+ fine-grained updates flow through the signals inside it.
854
+
855
+ **App-wide state is the same provider, placed at the root of `App`** — not a
856
+ module-level signal or store. Module scope is for constants and the `createContext`
857
+ call itself:
858
+
859
+ ```tsx
860
+ import { createContext, createStore, useContext, type ParentProps } from 'solid-js';
861
+
862
+ type CartItem = { id: string; quantity: number };
863
+
864
+ // WRONG under SSR — one store for every request the server ever handles
865
+ // export const [cart, setCart] = createStore({ items: [] as CartItem[] });
866
+
867
+ // CORRECT — one store per provider instance: per tab in the browser, per request on the server
868
+ function createCart() {
869
+ const [cart, setCart] = createStore({ items: [] as CartItem[] });
870
+ return { cart, setCart };
871
+ }
872
+
873
+ const CartContext = createContext<ReturnType<typeof createCart>>();
874
+
875
+ export function CartProvider(props: ParentProps) {
876
+ return <CartContext value={createCart()}>{props.children}</CartContext>;
877
+ }
878
+
879
+ export function useCart() {
880
+ return useContext(CartContext); // throws ContextNotFoundError outside CartProvider
881
+ }
882
+
883
+ // App: <CartProvider><ThemeProvider>{/* router / pages */}</ThemeProvider></CartProvider>
884
+ ```
885
+
886
+ Module state has no owner (nothing disposes it), and under `ssr: true` the server
887
+ loads the module once and serves every request from it: `createStore(value)` on the
888
+ server is the plain object, so one visitor's writes render into the next visitor's
889
+ HTML (`[SERVER_WRITE]`). A module-level store derived from a server function fails
890
+ sooner — `Cannot call server function outside of a request` while the module loads.
891
+ In a client-only build a module-level store is one per tab and works, which is why it
892
+ looks fine until `ssr: true` is turned on; the provider costs nothing extra and
893
+ survives that change.
778
894
 
779
895
  ### Refs
780
896
 
@@ -815,6 +931,32 @@ do not `return () => cleanup`. Put owned setup in a **directive factory** (like
815
931
  `ResizeObserver`, third-party widgets, and native listener options (`capture` /
816
932
  `passive`) — Solid event props do not take those options.
817
933
 
934
+ ### `onSettled` — one-time setup, restricted scope
935
+
936
+ `onSettled` is the `onMount` replacement, and its callback is a **restricted scope**:
937
+ `onCleanup` throws there (`[CLEANUP_IN_FORBIDDEN_SCOPE]`), creating a memo or effect
938
+ throws (`[PRIMITIVE_IN_FORBIDDEN_SCOPE]`), and so does `flush()` — each halts the
939
+ reactive system in dev. Return the cleanup; create effects and memos in the component
940
+ body, next to the `onSettled` call, not inside it.
941
+
942
+ ```tsx
943
+ // WRONG — the mechanical 1.x port of onMount + onCleanup
944
+ onSettled(() => {
945
+ const id = setInterval(tick, 1000);
946
+ onCleanup(() => clearInterval(id));
947
+ });
948
+
949
+ // CORRECT
950
+ onSettled(() => {
951
+ const id = setInterval(tick, 1000);
952
+ return () => clearInterval(id);
953
+ });
954
+ ```
955
+
956
+ Effect callbacks follow the same shape: return the cleanup from `apply` (an
957
+ `onCleanup` there never runs), and `flush()` inside one is a no-op
958
+ (`[FLUSH_IN_EFFECT_CALLBACK]`).
959
+
818
960
  ### Per-value SSR policy: `ssrSource`
819
961
 
820
962
  `clientOnly` splits a *component*; `ssrSource` keeps one *value* off the server,
@@ -893,6 +1035,14 @@ test('increments on click', () => {
893
1035
  });
894
1036
  ```
895
1037
 
1038
+ Primitive tests write **outside** the root callback — a write inside `createRoot((dispose)
1039
+ => { …; setA(2); })` is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` in dev. Return the handles, then
1040
+ write and `flush()`: `const { setA, b, dispose } = createRoot((dispose) => { …; return
1041
+ { setA, b, dispose }; }); setA(2); flush();`. A hook that reads context runs under
1042
+ `renderHook(useCart, { wrapper: CartProvider })`. A routed component gets a router built in
1043
+ the test — `createRouter({ routes, history: memoryHistory("/orders/42") })` — because
1044
+ Testing Library's `render(…, { location })` is typed but not implemented (1.0.0-beta.3).
1045
+
896
1046
  `jsxImportSource` for tests and app code is `"@solidjs/web"`, not `"solid-js"`.
897
1047
  Vite plugin is `@solidjs/vite-plugin`, not `vite-plugin-solid`.
898
1048
 
@@ -1151,22 +1301,28 @@ Solid Router 0.x/1.x stand-ins.** Patterns below; fetch the matching pages from
1151
1301
  ```
1152
1302
 
1153
1303
  `solid-js` does **not** export a `JSX` namespace — `import type { JSX } from "solid-js"`
1154
- is a type error (TS2305), and `JSX.Element` as the children type is a React reflex.
1155
- Children/return types are the renderer-neutral `Element` from `solid-js`
1304
+ is a type error (TS2305). Markup types are `JSX.Element` from `@solidjs/web` (the
1305
+ docs' choice; it also admits DOM nodes) or the renderer-neutral `Element` from `solid-js`
1156
1306
  (`children?: Element`), alongside `Component` / `ParentProps` / `Accessor` / `Setter`.
1157
- DOM-specific `JSX` types (`JSX.IntrinsicElements`, `JSX.CSSProperties`) and
1158
- `ComponentProps` come from `@solidjs/web`. Vite: `import solid from "@solidjs/vite-plugin"`
1307
+ Import the latter as `import type { Element as SolidElement } from "solid-js"` in any file
1308
+ that also uses DOM elements — unaliased, it shadows the DOM `Element`
1309
+ (`el.getBoundingClientRect()` becomes TS2339). DOM-specific `JSX` types
1310
+ (`JSX.IntrinsicElements`, `JSX.CSSProperties`) and `ComponentProps` come from
1311
+ `@solidjs/web`. Vite: `import solid from "@solidjs/vite-plugin"`
1159
1312
  with `start: true` / `ssr: true` / `serverFunctions: true` only when the
1160
1313
  project already uses start mode. Tiers are `bare` → `basic` (router) →
1161
1314
  `fullstack` (SSR + server functions); do not jump a tier without cause.
1162
- Under `start: true` the plugin owns entries, the document shell, and the build —
1163
- write `src/App.tsx`, not SolidStart-style `entry-client.tsx` / `entry-server.tsx`
1164
- or a hand-written `index.html`.
1315
+ Under `start: true` the plugin owns entries and the build — write `src/App.tsx`, not
1316
+ SolidStart-style `entry-client.tsx` / `entry-server.tsx` (authored ones replace the
1317
+ generated entries) or a hand-written `index.html`. The document shell (`<html lang>`,
1318
+ favicon, fallback `<title>`) is an optional `src/Document.tsx` default export that renders
1319
+ `<HydrationScript />` in `<head>` and `<body>{props.children}</body>`; `App` never renders
1320
+ `<html>`.
1165
1321
 
1166
1322
  ### Solid Router 2 — `createRouter`, not JSX `<Route>`
1167
1323
 
1168
1324
  ```tsx
1169
- import { lazy } from 'solid-js';
1325
+ import { Loading, lazy } from 'solid-js';
1170
1326
  import { createRouter } from '@solidjs/router';
1171
1327
 
1172
1328
  export const Router = createRouter({
@@ -1184,7 +1340,9 @@ export default function App() {
1184
1340
  {(props) => (
1185
1341
  <>
1186
1342
  <a href={Router.paths}>Home</a>
1187
- <main>{props.children}</main>
1343
+ <Loading fallback={<p>Loading…</p>}>
1344
+ <main>{props.children}</main>
1345
+ </Loading>
1188
1346
  </>
1189
1347
  )}
1190
1348
  </Router>
@@ -1192,7 +1350,9 @@ export default function App() {
1192
1350
  }
1193
1351
  ```
1194
1352
 
1195
- Create the instance at **module scope**. Nested layouts are `children` arrays
1353
+ The `<Loading>` covers the first load of the lazy pages (and lets SSR stream the shell);
1354
+ later navigations hold the current page while the next one loads (links get
1355
+ `data-pending`). Create the instance at **module scope**. Nested layouts are `children` arrays
1196
1356
  on the route objects, not nested `<Route>` / nested routers. Solid Router does
1197
1357
  **not** support nested `<Router>` instances — compose one route tree (or a lazy
1198
1358
  `children` thunk). Navigate with
@@ -1200,8 +1360,57 @@ plain `<a href={Router.paths.about}>` (or `useNavigate`); there is no `<A>` /
1200
1360
  `<Navigate>` / `<HashRouter>` / `<FileRoutes />`. Do not assign
1201
1361
  `window.location.href` or call `history.pushState` for in-app navigation.
1202
1362
  Session location is `useLocation` / `useParams`,
1203
- not `Router.paths`. A route `preload` result is `props.data` on the matched
1204
- component (and the factory `preload` result is the root render prop's `props.data`).
1363
+ not `Router.paths`.
1364
+
1365
+ **Route `preload` starts reads; the page reads them in a memo.** `preload` runs when
1366
+ the route matches (and on link hover), so it should kick off the query and return
1367
+ nothing; the component reads the same query through a memo that tracks the params:
1368
+
1369
+ ```tsx
1370
+ // src/data/products.ts
1371
+ import { query } from '@solidjs/router';
1372
+
1373
+ export const getProduct = query(async (id: string) => {
1374
+ const response = await fetch(`/api/products/${id}`);
1375
+ return (await response.json()) as Product;
1376
+ }, 'product');
1377
+
1378
+ // src/router.ts — defineRoute types params from the path (params.id: string)
1379
+ import { lazy } from 'solid-js';
1380
+ import { createRouter, defineRoute } from '@solidjs/router';
1381
+ import { getProduct } from './data/products';
1382
+
1383
+ export const Router = createRouter({
1384
+ routes: [
1385
+ defineRoute({
1386
+ path: '/products/:id',
1387
+ preload: ({ params }) => void getProduct(params.id),
1388
+ component: lazy(() => import('./pages/Product')),
1389
+ }),
1390
+ ],
1391
+ });
1392
+
1393
+ // src/pages/Product.tsx
1394
+ import { createMemo } from 'solid-js';
1395
+ import type { RouteProps } from '@solidjs/router';
1396
+ import { getProduct } from '../data/products';
1397
+
1398
+ export default function ProductPage(props: RouteProps<'/products/:id'>) {
1399
+ const product = createMemo(() => getProduct(props.params.id));
1400
+ return <h1>{product().name}</h1>;
1401
+ }
1402
+ ```
1403
+
1404
+ Do not return the data from `preload` and render `props.data` (the Remix-loader
1405
+ shape): `props.data` is whatever `preload` returned when the route first matched, so
1406
+ navigating `/products/mug` → `/products/bowl` keeps showing the mug, and an `async`
1407
+ preload makes it a Promise (`props.data.name` is TS2339). Return a value only for
1408
+ data that stays fixed while the route is matched. The factory `preload` on
1409
+ `createRouter` runs once per mount/request; its result is the root render prop's
1410
+ `props.data`. Plain route objects type `params` as an open record (`string |
1411
+ undefined` per key) — wrap a route in `defineRoute` when `preload` or the component
1412
+ needs `params.id: string`.
1413
+
1205
1414
  Link warming: `preload="false"` skips that link's *data* preload; `preloadLinks: false`
1206
1415
  disables automatic link preloading. Intent values are `"initial"` / `"navigate"` /
1207
1416
  `"native"` / `"preload"`. One router per app.
@@ -1225,7 +1434,9 @@ setSearch] = useSearchParams()` — the setter merges and navigates without scro
1225
1434
  `""` / `undefined` / `null` removes a key. For typed values, put a synchronous
1226
1435
  Standard Schema on the route's `search` field and pass the path node:
1227
1436
  `useSearchParams(Router.paths.search)` (async schemas throw; a failing schema leaves
1228
- the raw strings rather than throwing).
1437
+ the raw strings rather than throwing). Query values are strings, so coerce in the schema —
1438
+ `page: v.optional(v.pipe(v.unknown(), v.transform(Number)), 1)`; a plain `v.number()`
1439
+ rejects every value and hands back the string, still typed `number`.
1229
1440
 
1230
1441
  **Two different `action`s.** Core `action` from `solid-js` is a generator transaction
1231
1442
  (optimistic writes that span an async gap). Router `action` from `@solidjs/router` is
@@ -1250,6 +1461,14 @@ const save = action(async (form: FormData) => {
1250
1461
  Only POST forms are accepted. Bind extra args with `.with(id)` (they go in the
1251
1462
  action URL). `onSubmit={(e) => { e.preventDefault(); fetch(...) }}` *runs* and
1252
1463
  drops the no-JS form fallback. `useAction` is JS-only for the same reason.
1464
+ Optimistic rows for a router form keep the `<form>` and add no core `action`: read the
1465
+ list `query` with `createOptimisticStore(() => getList(), [])` and register
1466
+ `save.onSubmit((form) => setList((d) => { d.push({ …, pending: true }); }))` in the
1467
+ component — the hook runs inside the action's transaction, so the row is an overlay,
1468
+ replaced on settle by the revalidated `query` (a plain server-function read is not
1469
+ revalidated, so the row just disappears).
1470
+ `useSubmissions` keeps only completions with a result or error; observe void and
1471
+ redirect completions with `save.onSettled(hook)`.
1253
1472
 
1254
1473
  Validated forms come in three passes sharing one HTML shape. Pass 1 (above) posts
1255
1474
  uncontrolled inputs with browser `required` / `pattern` — it works with no JavaScript.
@@ -1349,7 +1568,15 @@ A function-level server function cannot close over component locals — pass
1349
1568
  values as arguments. Treat every argument as untrusted — TypeScript does not
1350
1569
  cross the HTTP boundary, so do not invent tRPC / RPC type-gen;
1351
1570
  validate inside the function (a parse in the function body is that
1352
- validation, not an RPC schema across the wire). These functions *are* the RPC — do not add an
1571
+ validation, not an RPC schema across the wire). Checks belong in the body: HTTP dispatch
1572
+ calls a function-level `"use server"` body directly, so a HOF around it (`withAuth(fn)`,
1573
+ `validated(schema, fn)`) runs for in-process calls and is skipped for `curl` — an auth
1574
+ bypass. A wrapper inside a file-level `"use server"` module (`export const x =
1575
+ withAccount(async …)`) is what gets registered and does run. Arguments are capped at 1 MiB
1576
+ by default (413 beyond; the host's `bodySizeLimit`) — upload large files to a signed URL.
1577
+ Never hand a server function point-free to a library callback (`mutationFn: renameUser`,
1578
+ `onClick={deleteTodo}`): every argument the callback receives (a context object, the
1579
+ event) is sent over the wire — wrap it in an arrow that passes only the data. These functions *are* the RPC — do not add an
1353
1580
  API-route file (or a Next `route.ts`) just to wrap `db.todos.insert`. API
1354
1581
  routes are for real HTTP endpoints (webhooks, third-party POST), not for the
1355
1582
  app's own RPC. Read trusted identity
@@ -1358,8 +1585,42 @@ reference runs in-process (no HTTP); in the browser it is HTTP. Anything
1358
1585
  referenced only inside the `"use server"` body (db client, secrets) never
1359
1586
  reaches the client — the directive is the privacy boundary. Mutations that should
1360
1587
  revalidate: `redirect` / `reload` from `@solidjs/web` (see the server-function
1361
- guides). Return or throw a redirect — guards read `if (!session?.userId) throw
1362
- redirect("/sign-in")`. `redirect()` and `respond()` accept the same `revalidate`
1588
+ guides).
1589
+
1590
+ **`redirect()` / `reload()` are signals for an integration, not navigation.** Solid
1591
+ Router's `action` / `query` apply them (navigate, revalidate), and a no-JS form post
1592
+ follows the 302. Any other caller — a core `action`, an event handler, a plain
1593
+ `createMemo(() => listOrders())` — receives the raw `Response` as the resolved value:
1594
+ no navigation happens, and the type still claims your data. (When a mounted router's
1595
+ single-flight hook intercepts a direct POST call, it does navigate, but the call
1596
+ resolves to `null`; a direct `GET()` read always gets the `Response`.) Either way the
1597
+ caller never gets its data, so the redirect guard belongs to functions called through
1598
+ the router:
1599
+
1600
+ ```ts
1601
+ import { getRequestEvent, redirect, respond } from '@solidjs/web';
1602
+
1603
+ // called through a router action/query: the router navigates to /sign-in
1604
+ export async function listOrders() {
1605
+ 'use server';
1606
+ const userId = getRequestEvent()?.locals.userId;
1607
+ if (!userId) throw redirect('/sign-in');
1608
+ return database.orders.forUser(userId);
1609
+ }
1610
+
1611
+ // no router in the project (core action + server functions): reject instead
1612
+ export async function listMyOrders() {
1613
+ 'use server';
1614
+ const userId = getRequestEvent()?.locals.userId;
1615
+ if (!userId) throw respond(null, { status: 401 });
1616
+ return database.orders.forUser(userId);
1617
+ }
1618
+ ```
1619
+
1620
+ Without the router, fail with `throw respond(..., { status })` (the call rejects and
1621
+ `<Errored>` / the action's `catch` sees it), or return a value and navigate in the
1622
+ caller. Return a redirect when it is the successful outcome, throw it for an early
1623
+ exit. `redirect()` and `respond()` accept the same `revalidate`
1363
1624
  option as `reload()` when the mutation also changes location or returns a value;
1364
1625
  `revalidate: REVALIDATE_ALL` (`"*"`, from `@solidjs/web`) declares every cache entry
1365
1626
  stale, distinct from omitting it (the host's default) and from `[]` (nothing). After a router/server mutation do **not** follow with a client `fetch`
@@ -1370,11 +1631,17 @@ on the server-function wire **and**, since 2.0.0-rc.9, on every SSR road a failu
1370
1631
  takes (an `<Errored>` fallback rendered on the server, a rejected async source
1371
1632
  serialized into the stream, a `<Loading>` fragment's rejection), so `err().message`
1372
1633
  in a fallback reads the generic message in a production server render. Use
1373
- `markSafeError` or `respond(..., { status })` for intentional client-facing
1374
- failures. Do not `return Response.json(...)` from `"use server"` — that is
1375
- HTTP-handler control flow; `respond(value, { status })` is what a scripted
1376
- caller unwraps. JSON-encodable arguments only unless `enableRichArguments()`
1377
- was called once in the client entry (`Date` / `Map` / `Set` throw without it).
1634
+ `throw markSafeError(...)` or `throw respond(..., { status })` for intentional
1635
+ client-facing failures — a *returned* `respond()` resolves the call whatever its
1636
+ status, so `return respond({ error }, { status: 400 })` reads as success to the UI,
1637
+ `useSubmissions`, and the optimistic overlay. Do not `return Response.json(...)` from
1638
+ `"use server"` — that is HTTP-handler control flow; `return respond(value, { status: 201 })`
1639
+ is success metadata a scripted caller unwraps. Arguments are JSON: send `Date` / `Map` /
1640
+ `Set` as ISO strings / arrays (they throw otherwise; one `File` / `Blob` / `FormData`
1641
+ argument travels natively). `enableRichArguments()` from
1642
+ `@solidjs/web/server-functions/rich-args` (at `src/App.tsx` module scope) lifts that, but
1643
+ in rc.9 importing it fails `vite build` (`"./client" is not exported` — the dev server
1644
+ works, so the break shows only at build).
1378
1645
  `GET()` is only for idempotent reads (URLs leak into logs/history);
1379
1646
  import it from `@solidjs/web/server-functions`, never `@solidjs/start`.
1380
1647
  Reads are function calls too: `GET(async (id) => { "use server"; ... })` is invoked
@@ -1467,19 +1734,33 @@ When the app has a server bundle (`ssr` or server functions), production is
1467
1734
  `handleRequest(request)` from the built `dist/server/server.js` (or that
1468
1735
  module's Fetchable default `fetch`) — not an import from `solid-js`. Cloudflare /
1469
1736
  Netlify / Nitro Vite plugins adopt that handler — do not write a Solid adapter,
1470
- a custom Worker, or a Netlify Function. For Node, use the official template
1471
- `server.js` (web `Request` in, stream the response); do not invent an
1472
- Express/Solid bridge. Client-only `start: true` is still static `dist/client`
1737
+ a custom Worker, or a Netlify Function. For Node, set `start: { node: true }` and run
1738
+ `node dist/server/node.js` (serves `dist/client`, reads `PORT` / `HOST`); inside an
1739
+ existing Express / Fastify app, mount the `listener` that file exports — do not
1740
+ hand-write a Node↔web bridge (`node dist/server/server.js` starts nothing: it is the
1741
+ handler module). Client-only `start: true` is still static `dist/client`
1473
1742
  — there is no handler to wrap. Request middleware is the Vite option
1474
1743
  `start: { middleware: "./src/middleware.ts" }` (web `Request` + `next`), not
1475
1744
  Express `app.use`. Server functions that return components
1476
1745
  (`serverFunctions: { components: true }`) are experimental preview — do not use
1477
1746
  them unless the project already has that flag on.
1478
1747
 
1479
- Unscripted forms: `<form method="post" action={createTodo.url}>` (the reference
1480
- `.url`), not a client `preventDefault` + `fetch`, and not a hand-built
1481
- `/_server/` URL. GET forms (`method="get" action={search.url}`) are only for
1482
- idempotent search — do not use a GET form for a mutation.
1748
+ Unscripted (no-JS) POST forms go through a router action —
1749
+ `const save = action(createTodo, "create-todo")` from `@solidjs/router`, then
1750
+ `<form action={save} method="post">` — not a client `preventDefault` + `fetch`, not a
1751
+ hand-built `/_server/` URL, and not `action={createTodo.url}`: a bare `"use server"`
1752
+ function keeps its declared function type, so `.url` is a type error (TS2339) even
1753
+ though it exists at runtime (and `serverFunctionActionUrl(createTodo)` is TS2345).
1754
+ The post arrives as one `FormData` argument, so the function behind `action={save}`
1755
+ takes `(form: FormData)` and parses and validates the fields inside; any other
1756
+ signature makes `action={save}` a type error (TS2322). Bind leading JSON-safe
1757
+ arguments with `action={save.with(id)}` for `(id: string, form: FormData)`; a function
1758
+ that already has a typed signature gets a separate `FormData` server function that
1759
+ parses and calls it.
1760
+ `.url` is typed only on `GET()` / `live()` references, which makes it the address of a
1761
+ `method="get"` search form (`action={searchProducts.url}`; the function receives the
1762
+ fields as `URLSearchParams`). GET forms are only for idempotent search — do not use a
1763
+ GET form for a mutation.
1483
1764
 
1484
1765
  ### Document head — no MetaProvider
1485
1766
 
@@ -1515,8 +1796,11 @@ how to reuse. Prefer the form on the right.
1515
1796
  | `action(async function* () { await save(); setX(v); })` | `yield save()` or `await save(); yield; setX(v)` |
1516
1797
  | `action(async (item) => { ... })` with core `action` from `solid-js` | `action(function* (item) { ...; yield ...; ... })` — only a generator re-enters the transaction (`solid2-kit check` flags this). Router `action` from `@solidjs/router` is the one that takes `async (form) => ...` |
1517
1798
  | `todos()` on a store | `todos.length` / `todo.title` (property reads) |
1799
+ | `todos.push(x)` / `state.count++` / `todo.done = !todo.done` on the store itself | `setTodos((draft) => { draft.push(x); })` — writes to the store proxy outside a setter are silently ignored (no error, no warning, tsc passes) |
1800
+ | `createStore({ selected: new Set<string>() })` + `draft.selected.add(id)` | `selected: {} as Record<string, true>` + `draft.selected[id] = true` / `delete draft.selected[id]`, or `draft.selected = new Set([...draft.selected, id])` — `Map` / `Set` / `Date` are stored as-is, not tracked |
1518
1801
  | `draft.todos = fresh` (wholesale fresh-tree assignment) | `reconcile(fresh, "id")`, or function-form `createStore` / `createProjection` (auto-keyed by `"id"`). Removal via `setTodos((t) => t.filter(...))` is fine — survivors keep identity |
1519
- | `createEffect(() => user, (u) => log(u.name))` | `createEffect(() => user.name, (name) => log(name))` |
1802
+ | `createEffect(() => user, (u) => { log(u.name); })` | `createEffect(() => user.name, (name) => { log(name); })` |
1803
+ | `createEffect(() => title(), (t) => (document.title = t))` / `(v) => setX(v)` | `(t) => { document.title = t; }` — apply returns a cleanup or nothing; any other return value halts reactivity. An apply that only calls a local setter is rule 4 even with braces (`solid2-kit check` flags it): use a (writable) derivation |
1520
1804
  | `<Loading fallback={<PageSkeleton />}>{/* header + data */}</Loading>` | wrap only the data slot; chrome stays outside |
1521
1805
  | `<Loading on={id} fallback={...}>` (the accessor) | `on={id()}` — a value, so identity changes can show fallback |
1522
1806
  | `setCount(count() + 1)` when writes can batch | `setCount((c) => c + 1)` |
@@ -1553,15 +1837,17 @@ how to reuse. Prefer the form on the right.
1553
1837
  | `clientOnly` split (or `isServer` branch) for one browser-only *value* | `createMemo(() => localStorage.getItem(k), { ssrSource: "client" })` — `"hybrid"` when server data mixes with client signals |
1554
1838
  | `await flush()` / `flush()` to wait on a pending memo | `await resolve(() => value())` (or Testing Library async queries); `flush()` only drains staged *sync* writes |
1555
1839
  | `action(function* () { setX(v); })` around a navigation-shaped setter | plain setter; async computeds hold previous values. Core `action` only when writes happen *after* async work |
1556
- | `createContext` + a root provider for a true app singleton | module-level signal/store (SSR: shared across requests). Context is subtree scoping |
1840
+ | `export const [cart, setCart] = createStore(...)` for app-wide state | `createCart()` called by a `CartProvider` at the root of `App`, read with `useCart()` over `createContext<T>()` — module state has no owner and, under SSR, one instance serves every request (`[SERVER_WRITE]`). Module scope is for constants and `createContext` |
1557
1841
  | `import { GET } from "@solidjs/start"` / `createAsync` / `useSubmission` / `<FileRoutes />` / `"use client"` | `GET` from `@solidjs/web/server-functions`; `createMemo(() => getUser(id()))`; `useSubmissions`; `fileRoutes(pageRoutes)`; Solid has no `"use client"` |
1558
1842
  | `<Show keyed when={user()}>` by default | default `Show` (keeps children mounted); `keyed` only when internal state must reset |
1559
1843
  | overlapping truthy `<Match>`es all expected to render | first truthy `<Match>` wins; later matches are skipped |
1560
1844
  | missing `<HydrationScript />` / one script for many roots | once, before app markup, when the app owns the document; distinct `renderId` per extra root |
1561
- | `Router.paths` as the current URL; calling `preload()` and using the return as props | `useLocation` / `useParams`; factory/route `preload` result is `props.data` |
1845
+ | `Router.paths` as the current URL | `useLocation` / `useParams` |
1846
+ | `preload: async ({ params }) => getProduct(params.id)` + `props.data.name` in the page | `preload: ({ params }) => void getProduct(params.id)` + `createMemo(() => getProduct(props.params.id))` — `props.data` is captured once when the route matches (stale on param change; a Promise for async preloads) |
1562
1847
  | `query.get(key)` with no guaranteed entry; `query.set(key, promise)` | `query.get` throws if missing; `query.set` does not accept a promise |
1563
1848
  | `onCleanup(() => ...)` in a component body | `onSettled(() => { ...; return cleanup })` — `onCleanup` is for custom primitives |
1564
- | `onSettled` with reactive reads expecting re-runs | each call is a **single** fire; reads are untracked. Ongoing work is `createEffect` |
1849
+ | `onSettled` with reactive reads expecting re-runs | each call is a **single** fire; reads are untracked. Ongoing work is `createEffect`, created in the component body |
1850
+ | `onSettled(() => { const id = setInterval(tick, 1000); onCleanup(() => clearInterval(id)); })` | `onSettled(() => { const id = setInterval(tick, 1000); return () => clearInterval(id); })` — `onCleanup`, `createMemo` / `createEffect`, and `flush()` throw inside the callback |
1565
1851
  | `<Router>` nested inside `<Router>` | one instance; nest `children` arrays (or a lazy children thunk) |
1566
1852
  | `const View = tab() ? A : B; return <View />` | `const View = dynamic(() => (tab() ? A : B))` — the ternary at setup freezes the choice |
1567
1853
  | `<For each={resolved()}>` after `children()` | `{resolved.toArray()}` — resolved children are not a reactive data list |
@@ -1571,8 +1857,8 @@ how to reuse. Prefer the form on the right.
1571
1857
  | `createOptimistic` for a local edit draft | writable derivation / plain store. Optimistic is for an in-flight mutation |
1572
1858
  | `createMemo(async fn, { loadingValue })` / `{ seedLoadingValue: true }` as the default first-flight UI | `<Loading>` for first flight; those options are escape hatches (store projections use `seedLoadingValue`) |
1573
1859
  | `setSubmitted(true)` + an effect that watches it | do the work in the handler or an `action` |
1574
- | `findUser(userId)` with the id from the client as identity | `getRequestEvent()!.locals.userId` |
1575
- | `action={"/_server/" + id}` in app JSX | `action={createTodo.url}` or a router `action` |
1860
+ | `findUser(userId)` with the id from the client as identity | `const userId = getRequestEvent()?.locals.userId; if (!userId) throw respond(null, { status: 401 });` (`throw redirect("/sign-in")` under a router `action` / `query`) |
1861
+ | `action={"/_server/" + id}` / `action={createTodo.url}` on a POST form | a router action: `const save = action(createTodo, "create-todo")` + `<form action={save} method="post">` over a `(form: FormData)` server function (other signatures are TS2322) — `.url` on a bare `"use server"` function is TS2339; it is typed only on `GET()` / `live()` references (GET search forms) |
1576
1862
  | `<form method="get" action={update.url}>` for a mutation | GET forms only for idempotent search; mutations are POST |
1577
1863
  | `window.location.href = ...` / `history.pushState` | `useNavigate()` or `<a href={Router.paths...}>` |
1578
1864
  | `class={{ active: location.pathname === "/about" }}` hand-rolled link state | CSS on automatic `aria-current` / `data-active` / `data-pending`; `useLinkState` in JSX, `useIsRouting()` for progress bars |
@@ -1587,7 +1873,7 @@ how to reuse. Prefer the form on the right.
1587
1873
  | `class={{ pending: isPending(id) }}` flashing on every navigation | same class + a short CSS `transition-delay` so only slow swaps show |
1588
1874
  | `<Comments />` nested under `{story().title}` assumed to waterfall | both mount immediately and fetch in parallel if the child reads an already-known id; a real waterfall is `createMemo(() => fetchAuthor(story().authorId))` or `storyId={props.story.id}` |
1589
1875
  | `<Errored>` as a terminal ErrorBoundary; `reset={() => location.reload()}` | the boundary heals when the source succeeds again; `reset` retries collected *sources*, not a UI remount |
1590
- | `<Errored>` around a list catching a toggle/save failure | catch expected mutation failures in the `action`; keep row errors in a map the projection folds in (survives overlay discard) |
1876
+ | `<Errored>` around a list expected to catch a toggle/save failure | an error thrown out of an `action` never reaches `<Errored>` — it reverts the overlay and rejects the call's promise. Catch expected failures in the `action` (row errors in a map the projection folds in, which survives overlay discard; otherwise a toast) and `.catch` the call in the handler |
1591
1877
  | `yield { ...tick, connected: true }` from `live()` | yield the value; wire status is `source.onstatus` (`"connected"` / `"reconnecting"` / `"closed"`) |
1592
1878
  | sibling `<Loading>` fallbacks stacking as popcorn | `<Reveal collapsed>` (sequential default) suppresses tail skeletons past the frontier |
1593
1879
  | in-memory cache around a GET server function | `return respond(value, { headers: { "cache-control": "..." } })` — metadata rides HTTP, the value rides the graph |
@@ -1599,6 +1885,7 @@ how to reuse. Prefer the form on the right.
1599
1885
  | a held write with no feedback anywhere | pair it with `isPending()` / `latest()` / an optimistic value / `affects()` — otherwise dev + attribution reports `[SILENT_HOLD]` |
1600
1886
  | `refresh(getUser.key)` / `revalidate(user)` / `return refresh()` from `"use server"` | three APIs: core `refresh(source)` reruns a reactive source; router `revalidate(getUser.key)` invalidates the query cache; `return reload({ revalidate: "todos" })` asks the integration to refresh cached data |
1601
1887
  | `return redirect(...)` as the only redirect shape | guards `throw redirect("/sign-in")`; `redirect()` / `respond()` take `revalidate` too when the mutation also moves or returns a value |
1888
+ | `throw redirect("/sign-in")` / `return reload(...)` in a function called directly (core `action`, handler, plain memo) | only Solid Router `action` / `query` (and no-JS form posts) apply them — a direct caller resolves to the raw `Response` (or `null` when the router's single-flight hook intercepts a POST call), typed as your data. Without the router: `throw respond(null, { status: 401 })` or return a value and navigate in the caller |
1602
1889
  | `submission.clear()` from an effect | on dismiss/resubmit only — from an effect, errors flash and vanish |
1603
1890
  | `revalidate(liveSource)` | `live()` updates through the open stream; do not revalidate it |
1604
1891
  | `refresh(user)` without `affects` when the reload should look pending | pair `affects(user)` with `refresh(user)` — a bare `refresh` re-asks quietly |
@@ -1606,7 +1893,7 @@ how to reuse. Prefer the form on the right.
1606
1893
  | `setStore(async (draft) => { draft.x = await load(); })` | `const x = await load(); setStore((draft) => { draft.x = x; })` — the setter is a synchronous transaction; dev throws `[ASYNC_STORE_SETTER]` |
1607
1894
  | `setStore((d) => { d.items = seed; })` in a component body | seed through `createStore(seed)` / `createStore(() => props.items, fallback)` — a body-level store write is `[REACTIVE_WRITE_IN_OWNED_SCOPE]` |
1608
1895
  | `const saved = await api.save(); yield until(() => ...)` | `await api.save(); yield; yield until(() => ...)` — a bare `yield` re-enters the transaction before any reader is created, not only before writes |
1609
- | `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or `render(..., { onError })`), `configureServerErrors({ onError })` from `@solidjs/web` — once per error, with `ownerPath` / `boundaryPath` |
1896
+ | `fallback={(err) => { captureException(err()); return <Fallback />; }}` | `configureClientErrors({ onError })` from `solid-js` (or per root `render(fn, root, undefined, { onError })`), `configureServerErrors({ onError })` from `@solidjs/web` — once per error, with `ownerPath` / `boundaryPath` |
1610
1897
  | `latest(user) ?? placeholder` in a handler as a null-safe read | `latest()` throws `NotReadyError` before the first value in every scope; read settled values under `<Loading>`, or `await until(() => user())` |
1611
1898
  | `attribution.why(total)` / `attribution.format(e)` / `attribution.costs()` | named imports from `solid-js/attribution`: `why(total)`, `formatRerun(e)`, `costs()`, `feedback()`, `subscriptions(scope)` (rc.9) |
1612
1899
  | `attribution.enable()` unguarded in app code, or `enable()` with the default `log: true` left on | `if (isDev) attribution.enable({ log: false })` — on the observe build an unguarded `enable()` records in production; prefer the `/__solid/diagnostics` session (`begin` enables, `end` disables) so app code carries nothing |
@@ -1629,18 +1916,19 @@ how to reuse. Prefer the form on the right.
1629
1916
  | `fetch('/api')` at component-body top level | `createMemo(async () => { ... fetch ... })` under `<Loading>` — a top-level fetch runs once at mount and is not a reactive source |
1630
1917
  | `<input type="checkbox" value={on()} />` | `checked={on()}` for the toggle; `value="string"` only when grouping radios or listing submitted values |
1631
1918
  | `createMemo(() => { void save(); return x(); })` / an effect that calls an action | invoke actions from handlers, not from memos or effects |
1632
- | `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot |
1633
- | `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)` |
1919
+ | `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot. That form resets on every prop change; an intentional one-time read (an `initialCount` prop) is `createSignal(untrack(() => props.initialCount))` |
1920
+ | `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)`; `createTrackedEffect` is `@deprecated` (rc.9) |
1634
1921
  | `createEffect(() => source(), () => setError(null))` to reset a signal | `createSignal(() => { source(); return null; })` — the writable derivation resets on change; a sole-setter apply is rule 4 |
1635
1922
  | a "server-safe" initial value adopted after mount (React hydration-mismatch fix) | client mode mounts with `render()` into an empty body — no hydration, no mismatch; read `localStorage` / `matchMedia` directly at signal creation |
1636
- | rewrite `App.tsx` with loading/error branches, or snapshot/restore, when the list moves to the server | same `setTodos`; wrap mutations in `action`; swap `createOptimisticStore` + a server-functions file. The overlay is discarded; the confirmed store is the truth |
1923
+ | rewrite `App.tsx` with loading/error branches, or snapshot/restore, when the list moves to the server | same `setTodos`; wrap mutations in `action`; swap in function-form `createOptimisticStore(() => api.list(), [])` + a server-functions file. The overlay is discarded; the confirmed store is the truth |
1924
+ | `createOptimisticStore<Todo[]>([])` holding the list, saved rows written after `yield` | `createOptimisticStore(() => api.list(), [])` + `refresh(todos)` (or a function form over a separate `createStore`) — the value form is a pure overlay: action writes vanish on settle |
1637
1925
  | disable an optimistic row until ack, or mutex rapid clicks / freeze unrelated writes | keep the control enabled; `action` is the transaction. An in-flight action does not freeze other writes. Failed-action replay is optional |
1638
1926
  | tRPC / RPC type-gen around `"use server"` | TypeScript does not cross HTTP — validate inside the function. Locals only the body reads (db, secrets) stay off the client |
1639
1927
  | `src/routes/api/todos.ts` (or a Next `route.ts`) wrapping `db.todos.insert` | `"use server"` *is* the RPC. API routes are for real HTTP endpoints (webhooks), not the app's own mutations |
1640
1928
  | router/server mutation, then `fetch` / `onSettled` / `hydrate` to refresh the page | `reload` / `revalidate` / single-flight (the mutation response can seed destination preloads). In-flight Promises serialize as Promises; `live()` continues from HTML |
1641
1929
  | delay `renderToStream` so HTML arrives in visual order | stream as soon as ready; `<Reveal collapsed>` controls when content *appears* |
1642
1930
  | subscribe/unsubscribe in the component around `live()` | `createMemo(() => stockPrice(props.symbol))` — hoist that memo to share one connection. `onstatus` is on the iterable the `live()` call returns, not on the memo. Until the first yield, `<Loading>`; later yields are updates |
1643
- | a Solid adapter / custom Worker / Express bridge, or `entry-client.tsx` under `start: true` | With a server bundle: `handleRequest` from `dist/server/server.js` / Fetchable `fetch`; platform plugins adopt it. Node: template `server.js`. Client-only start: static `dist/client`. Middleware is `start: { middleware: "..." }`, not Express `app.use` |
1931
+ | a Solid adapter / custom Worker / Express bridge, or `entry-client.tsx` under `start: true` | With a server bundle: `handleRequest` from `dist/server/server.js` / Fetchable `fetch`; platform plugins adopt it. Node: `start: { node: true }` → `node dist/server/node.js` (its `listener` for an existing Express / Fastify app). Client-only start: static `dist/client`. Middleware is `start: { middleware: "..." }`, not Express `app.use` |
1644
1932
  | `createMemo(async () => (await props.story).author)` | `createMemo(() => props.story.author)` — a derivation over an async value becomes async; no await, no Promise type |
1645
1933
  | return a component from `"use server"` / flip `serverFunctions.components` | experimental preview — do not enable unless the project already has that flag |
1646
1934
 
@@ -1779,17 +2067,19 @@ always-applied rules installed alongside this skill.
1779
2067
  bind `latest(query)`; failures throw to `<Errored>`, never `{ success: false }`
1780
2068
  result objects checked at every read.
1781
2069
  - [ ] Lists via `<For>` (server/refetched rows keyed by stable id), conditionals via
1782
- ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props; no `value()!`.
2070
+ ternary/`<Show>`; no `{list().map(...)}` in JSX; no `key` props (except
2071
+ `@solidjs/meta` tags); no `value()!`.
1783
2072
  - [ ] Effects are two-phase and only at imperative boundaries; apply does not read stores.
1784
2073
  - [ ] Async reads sit under `<Loading>` (the data slot, not chrome); errors under `<Errored>`.
1785
2074
  Reactive inputs of an async memo are read before the first `await`. No hand-rolled
1786
2075
  `loading`/`error` signals or `=== undefined` readiness branches. First load is
1787
2076
  `<Loading>`; refetch indicator is `isPending(user)` (the accessor, not `user()`)
1788
2077
  or `isPending(selectedId)` when the write is the question. `<Errored>` heals
1789
- when the source succeeds; `reset` retries sources, not a UI remount. Per-row
1790
- mutation failures stay in the action/projection, not in `<Errored>` around
1791
- the list. Error monitoring goes through `configureClientErrors` /
1792
- `configureServerErrors` (or `render(..., { onError })`), never a side effect in
2078
+ when the source succeeds; `reset` retries sources, not a UI remount. Mutation
2079
+ failures never reach `<Errored>` (an escaping error reverts the overlay and
2080
+ rejects the call): catch them in the action/projection, and `.catch` the call
2081
+ in the handler. Error monitoring goes through `configureClientErrors` /
2082
+ `configureServerErrors` (or `render(fn, root, undefined, { onError })`), never a side effect in
1793
2083
  the fallback. Core `refresh(source)`, router `revalidate(key)`, and `return reload(...)` are
1794
2084
  different APIs — do not mix them. Client → server is additive (same
1795
2085
  setters + `action` + server functions): do not rewrite App with
@@ -1807,13 +2097,16 @@ always-applied rules installed alongside this skill.
1807
2097
  - [ ] Rest props via `omit`, not `{ ...props }`. Conditional `class` via objects, not string concat.
1808
2098
  - [ ] Input filters write back to the DOM on reject (inputs do not rewind).
1809
2099
  - [ ] No Solid 1.x imports or APIs (`solid-js/store`, `createResource`, `onMount`, `Suspense`, ...).
1810
- No `JSX` imported from `solid-js` — children/return types are `Element` from
1811
- `solid-js`; DOM `JSX` types come from `@solidjs/web`.
2100
+ No `JSX` imported from `solid-js` — markup types are `JSX.Element` from
2101
+ `@solidjs/web` or `Element` from `solid-js` (aliased next to DOM code).
1812
2102
  - [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
1813
2103
  component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
1814
2104
  - [ ] Browser-only code uses `isServer` / `clientOnly` (components) / `ssrSource`
1815
2105
  (values), not `typeof window`.
1816
- Component teardown is `onSettled` (return cleanup), not `onCleanup`.
2106
+ Component teardown is `onSettled` (return cleanup), not `onCleanup`; nothing
2107
+ inside the `onSettled` callback calls `onCleanup`, `flush()`, or creates a
2108
+ memo/effect. Effect `apply` functions have block bodies and return only a
2109
+ cleanup or nothing.
1817
2110
  - [ ] Dev console shows no diagnostics: reads in tracking scopes (no
1818
2111
  `[STRICT_READ_UNTRACKED]`), store setters synchronous and never called at
1819
2112
  body level (no `[ASYNC_STORE_SETTER]` / `[REACTIVE_WRITE_IN_OWNED_SCOPE]`), held