@pylonsync/create-pylon 0.9.2 → 0.10.1
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.
- package/package.json +1 -1
- package/templates/_root/AGENTS.md +11 -0
- package/templates/agency/AGENTS.md +11 -0
- package/templates/ai-chat/AGENTS.md +11 -0
- package/templates/ai-studio/AGENTS.md +11 -0
- package/templates/barebones/AGENTS.md +11 -0
- package/templates/chat/AGENTS.md +11 -0
- package/templates/consumer/AGENTS.md +11 -0
- package/templates/creator/AGENTS.md +11 -0
- package/templates/crm/AGENTS.md +11 -0
- package/templates/directory/AGENTS.md +11 -0
- package/templates/helpdesk/AGENTS.md +11 -0
- package/templates/inventory/AGENTS.md +11 -0
- package/templates/invoices/AGENTS.md +11 -0
- package/templates/local-service/AGENTS.md +11 -0
- package/templates/marketplace/AGENTS.md +11 -0
- package/templates/projects/AGENTS.md +11 -0
- package/templates/restaurant/AGENTS.md +11 -0
- package/templates/saas/AGENTS.md +11 -0
- package/templates/saas/app/dashboard/layout.tsx +9 -2
- package/templates/saas/app/dashboard/page.tsx +37 -14
- package/templates/saas/app/dashboard/settings/page.tsx +11 -4
- package/templates/shop/AGENTS.md +11 -0
- package/templates/todo/AGENTS.md +11 -0
- package/templates/waitlist/AGENTS.md +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/create-pylon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Scaffold a new Pylon app — realtime backend + web/mobile/expo frontends in one command. Run via `npm create @pylonsync/pylon@latest`.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -40,6 +40,17 @@ this pass before the user has to ask.
|
|
|
40
40
|
## Key gotchas
|
|
41
41
|
|
|
42
42
|
- **Policies deny by default; server functions bypass them.** Direct client CRUD (`/api/entities/*`) and sync are policy-checked. Functions run with full database access, so enforce trust with `ctx.auth` checks inside the handler.
|
|
43
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
47
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
48
|
+
const org = use(orgPromise);
|
|
49
|
+
const projects = use(projectsPromise);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
53
|
+
|
|
43
54
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
44
55
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` or `<Suspense>`. The HTTP head commits when the shell is ready. Status, headers, and cookies set from a suspended subtree are lost, and `redirect` or `notFound` thrown below a Suspense boundary are swallowed.
|
|
45
56
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are available on mutations and actions, not queries.** An `action` has no direct `ctx.db`; use `ctx.runQuery` or `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
package/templates/chat/AGENTS.md
CHANGED
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
package/templates/crm/AGENTS.md
CHANGED
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
package/templates/saas/AGENTS.md
CHANGED
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -36,8 +36,15 @@ export default function DashboardLayout({
|
|
|
36
36
|
if (!auth.tenant_id) {
|
|
37
37
|
return <>{children}</>;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// Every read is started before the first use(): calling them one at a time
|
|
40
|
+
// makes each wait for the last, since use() suspends on the first pending
|
|
41
|
+
// thenable. Never Promise.all them — a new pending promise each render means
|
|
42
|
+
// the page never returns (React error #482).
|
|
43
|
+
const mePromise = serverData.get<{ email?: string }>("User", auth.user_id);
|
|
44
|
+
const orgPromise = serverData.get<{ name?: string }>("Org", auth.tenant_id);
|
|
45
|
+
|
|
46
|
+
const me = use(mePromise);
|
|
47
|
+
const org = use(orgPromise);
|
|
41
48
|
// Longest matching NAV href wins so /dashboard/projects highlights
|
|
42
49
|
// Projects, not Overview.
|
|
43
50
|
const path = (url ?? "").split("?")[0];
|
|
@@ -27,13 +27,35 @@ export default function DashboardPage({ auth, response, serverData }: PageProps)
|
|
|
27
27
|
if (!auth.tenant_id) {
|
|
28
28
|
return <ProvisionWorkspace />;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
// Every read is STARTED here, before the first use(). Each serverData call
|
|
31
|
+
// returns a thenable the handle caches by key, and use() suspends on the
|
|
32
|
+
// first one still pending — so reading them one at a time makes each wait
|
|
33
|
+
// for the last. Six reads that way is six round trips; issued together it
|
|
34
|
+
// is one, and the replayed render finds each already cached.
|
|
35
|
+
//
|
|
36
|
+
// Do NOT reach for Promise.all. It builds a new, pending, uncached promise
|
|
37
|
+
// on every render, so use() suspends, re-renders, builds another, and the
|
|
38
|
+
// page never returns — React reports it as an async Client Component
|
|
39
|
+
// (minified error #482).
|
|
40
|
+
//
|
|
41
|
+
// The onboarding redirect below only needs `org`, so on that path the other
|
|
42
|
+
// reads are issued and thrown away. A redirect is the rare case; paying one
|
|
43
|
+
// round trip on every normal load to save four on a rare one is the wrong
|
|
44
|
+
// way round.
|
|
45
|
+
const mePromise = serverData.get<{ email?: string }>("User", auth.user_id!);
|
|
46
|
+
const orgPromise = serverData.get<{
|
|
47
|
+
name?: string;
|
|
48
|
+
onboardedAt?: string | null;
|
|
49
|
+
setupDismissedAt?: string | null;
|
|
50
|
+
}>("Org", auth.tenant_id);
|
|
51
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
52
|
+
const membersPromise = serverData.list<OrgMemberRow>("OrgMember");
|
|
53
|
+
const subsPromise = serverData.list<Subscription>("StripeSubscription");
|
|
54
|
+
const invitesPromise =
|
|
55
|
+
serverData.list<{ orgId: string; acceptedAt?: string | null }>("OrgInvite");
|
|
56
|
+
|
|
57
|
+
const me = use(mePromise);
|
|
58
|
+
const org = use(orgPromise);
|
|
37
59
|
// A workspace created outside the wizard (or one that abandoned it) gets
|
|
38
60
|
// sent through it once. Owners/admins only; a member who lands here just
|
|
39
61
|
// sees the dashboard.
|
|
@@ -43,21 +65,22 @@ export default function DashboardPage({ auth, response, serverData }: PageProps)
|
|
|
43
65
|
response.redirect("/onboarding");
|
|
44
66
|
return null;
|
|
45
67
|
}
|
|
46
|
-
const projects = use(
|
|
47
|
-
const members = use(serverData.list<OrgMemberRow>("OrgMember"));
|
|
68
|
+
const projects = use(projectsPromise);
|
|
48
69
|
// The OrgMember read policy returns this user's memberships across every org,
|
|
49
70
|
// so scope the count to the active workspace.
|
|
50
|
-
const memberCount =
|
|
71
|
+
const memberCount = use(membersPromise).filter(
|
|
72
|
+
(m) => m.orgId === auth.tenant_id,
|
|
73
|
+
).length;
|
|
51
74
|
// Active-plan badge from the workspace's Stripe subscription (Free until one
|
|
52
75
|
// exists). Scoped to the active tenant by the plugin's read policy.
|
|
53
|
-
const
|
|
54
|
-
const active = subs.find((s) =>
|
|
76
|
+
const active = use(subsPromise).find((s) =>
|
|
55
77
|
["active", "trialing", "past_due"].includes(s.status),
|
|
56
78
|
);
|
|
57
79
|
const plan = active ? active.plan : "free";
|
|
58
80
|
// Getting-started checklist, derived from real rows so it ticks itself off.
|
|
59
|
-
const
|
|
60
|
-
|
|
81
|
+
const pendingInvites = use(invitesPromise).filter(
|
|
82
|
+
(i) => i.orgId === auth.tenant_id && !i.acceptedAt,
|
|
83
|
+
).length;
|
|
61
84
|
const setup: SetupState | null = org?.setupDismissedAt
|
|
62
85
|
? null
|
|
63
86
|
: {
|
|
@@ -21,10 +21,17 @@ export default function SettingsPage({ auth, response, serverData }: PageProps)
|
|
|
21
21
|
response.redirect("/dashboard");
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// Every read is started before the first use(): calling them one at a time
|
|
25
|
+
// makes each wait for the last, since use() suspends on the first pending
|
|
26
|
+
// thenable. Never Promise.all them — a new pending promise each render means
|
|
27
|
+
// the page never returns (React error #482).
|
|
28
|
+
const orgPromise = serverData.get<OrgInfo>("Org", auth.tenant_id);
|
|
29
|
+
const mePromise = serverData.get<AccountInfo>("User", auth.user_id!);
|
|
30
|
+
const membersPromise = serverData.list<OrgMemberRow>("OrgMember");
|
|
31
|
+
|
|
32
|
+
const org = use(orgPromise);
|
|
33
|
+
const me = use(mePromise);
|
|
34
|
+
const memberCount = use(membersPromise).filter(
|
|
28
35
|
(m) => m.orgId === auth.tenant_id,
|
|
29
36
|
).length;
|
|
30
37
|
return (
|
package/templates/shop/AGENTS.md
CHANGED
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
package/templates/todo/AGENTS.md
CHANGED
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|