Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,18 @@ The stock level for a product is one such question.

```ts
// src/data/inventory.ts
import { GET, live } from "@solidjs/web/server-functions";

export const stockLevel = live(
GET(async function* (productId: string) {
"use server";
yield await inventory.count(productId);
for await (const change of inventory.subscribe(productId)) {
yield change.count;
}
})
);
import { live } from "@solidjs/web/server-functions";

export const stockLevel = live(async function* (productId: string) {
"use server";
yield await inventory.count(productId);
for await (const change of inventory.subscribe(productId)) {
yield change.count;
}
});
```

Place `live()` outside `GET()`.
`GET()` chooses the read transport, and `live()` wraps the call behavior around it, so it must be the outermost declaration.
`live()` does not imply `GET()`; a live source without it streams over `POST`, which is fine when a cacheable URL is of no use.
A live source is a read, so `live()` declares it `GET` itself; the connection uses the read transport without a separate `GET()`.

The source yields the current count first, every time it is invoked.
That is the contract a reconnect depends on: when the connection drops and the client calls again, the first yield replaces the stale answer, and no client-side cache is needed to fill the gap.
Expand Down Expand Up @@ -267,7 +263,7 @@ Check the server log for the status: the client retries only transient failures
- A `GET()` read puts its arguments in the URL; keep secrets out of them and read identity from the request event.
- Responses leave with `Cache-Control: no-store`; return `respond(value, { headers })` to opt a read into caching.
- Return an async iterable to stream events over one connection; ending the iteration in the browser ends the server producer.
- Declare a value that changes over time with `live(GET(fn))`, `live()` outermost, and yield the current value first on every invocation.
- Declare a value that changes over time with `live(fn)`, which declares it `GET` as well, and yield the current value first on every invocation.
- Read a live source through one memo and pass the value down; each call site that invokes the source opens its own connection.
- Use `onstatus` to show a reconnect; put data freshness in the yielded value.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ The `Avoid` version places any order for any caller; `requiresAuth` is a value o

Later `withMeta()` calls shallow-merge over earlier metadata.
`GET()` uses the same channel for its HTTP method, and the two compose in either order.
`live()` wraps the call behavior, so it goes outside both:
`live()` wraps the call behavior and declares `GET` itself, so it goes outside `withMeta()` and needs no `GET()`:

```ts
export const stockLevels = live(
withMeta(
GET(async function* () {
async function* () {
"use server";
yield* inventory.levels();
}),
},
{ channel: "inventory" }
)
);
Expand Down Expand Up @@ -224,7 +224,7 @@ Check the condition in the hook against the metadata on the reference, and move

- The defaults are a `POST` to `/_server/data/<id>` with the argument array as the body and `Cache-Control: no-store` on the response; change them only for a reason on this page.
- Attach static facts to a declaration with `withMeta()`; the server does not read them, so enforce policy in the body.
- Compose `withMeta()` and `GET()` in either order, and put `live()` outside both.
- Compose `withMeta()` and `GET()` in either order, and put `live()`, which implies `GET()`, outside `withMeta()`.
- Add session-dependent headers in one `prepareRequest` hook, configured once in the client entry before the first call.
- Move the endpoint with the plugin's `serverFunctions.endpoint` option so client and server stay matched.
- Pass `signal`, `keepalive`, or `priority` to one call through `invoke()`; anything longer-lived is refused with a pointer to its home.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,14 @@ The live source yields the reply so far, whole, on every write:
```ts
// src/data/replies.ts
import { getRequestEvent } from "@solidjs/web";
import { GET, live } from "@solidjs/web/server-functions";
import { live } from "@solidjs/web/server-functions";
import * as replies from "../server/replies";

export const watchReply = live(
GET(async function* (replyId: string) {
"use server";
const signal = getRequestEvent()?.request.signal;
for await (const reply of replies.watch(replyId, signal)) yield reply;
})
);
export const watchReply = live(async function* (replyId: string) {
"use server";
const signal = getRequestEvent()?.request.signal;
for await (const reply of replies.watch(replyId, signal)) yield reply;
});
```

The request's signal ends the watcher when the reader leaves; the job does not see it.
Expand Down Expand Up @@ -221,7 +219,7 @@ A projection rather than a memo keeps readers of `reply.status` out of every tex
`refresh(reply)` re-runs the projection, which opens a new connection; it does not touch the job.

Every call site that iterates a live source holds its own connection.
When the thread list and the thread both need the same reply, hoist the read, or declare it with Solid Router's [`liveQuery`](/reference/solid-router/data#livequery), which keeps one connection per key for every consumer; the spelling is `liveQuery(watchReply, "reply")`, and a function already declared with `live(GET())` passes through unchanged.
When the thread list and the thread both need the same reply, hoist the read, or declare it with Solid Router's [`liveQuery`](/reference/solid-router/data#livequery), which keeps one connection per key for every consumer; the spelling is `liveQuery(watchReply, "reply")`. `liveQuery` declares a server function `live()` itself, and one already declared `live()` passes through unchanged.

## Hold the bubble until the stream confirms

Expand Down Expand Up @@ -288,55 +286,49 @@ The `chat` example in the Solid repository is this section's reference implement
:::

When the markdown renderer, the highlighter, and the model client should stay on the server, the thread can be server markup that keeps changing.
The server function returns a component instead of data, and `live()` holds the response open while it re-renders:
The server function returns a component instead of data, and `liveQuery`, which declares it `live()`, holds the response open while it re-renders:

```tsx
// src/routes/thread.tsx
import { createMemo, onCleanup } from "solid-js";
import { getRequestEvent } from "@solidjs/web";
import { GET, live } from "@solidjs/web/server-functions";
import type { Slot } from "@solidjs/web/frames";
import { liveQuery } from "@solidjs/router";
import { stopReply } from "../data/replies";
import * as threads from "../server/threads";

type ComposerSlot = Slot<{ threadId: string; seen: string[] }>;

export const getThread = liveQuery(
live(
GET(async (id: string) => {
"use server";
return (props: { composer: ComposerSlot }) => {
const gone = new AbortController();
onCleanup(() => gone.abort());
getRequestEvent()?.request.signal.addEventListener("abort", () =>
gone.abort()
);
const view = createMemo(() => threads.watch(id, gone.signal));
return (
<article>
{view().messages.map((message) => (
<section data-id={message.id}>
<div innerHTML={renderMarkdown(message.text)} />
{message.status === "generating" && (
<form action={stopReply.with(message.id)} method="post">
<button type="submit">Stop</button>
</form>
)}
</section>
))}
<props.composer
$key={id}
threadId={id}
seen={view().messages.map((m) => m.id)}
/>
</article>
);
};
})
),
"thread"
);
export const getThread = liveQuery(async (id: string) => {
"use server";
return (props: { composer: ComposerSlot }) => {
const gone = new AbortController();
onCleanup(() => gone.abort());
getRequestEvent()?.request.signal.addEventListener("abort", () =>
gone.abort()
);
const view = createMemo(() => threads.watch(id, gone.signal));
return (
<article>
{view().messages.map((message) => (
<section data-id={message.id}>
<div innerHTML={renderMarkdown(message.text)} />
{message.status === "generating" && (
<form action={stopReply.with(message.id)} method="post">
<button type="submit">Stop</button>
</form>
)}
</section>
))}
<props.composer
$key={id}
threadId={id}
seen={view().messages.map((m) => m.id)}
/>
</article>
);
};
}, "thread");
```

`threads.watch` is the thread-level watcher: it yields the thread and its messages after every write to any of them.
Expand Down Expand Up @@ -420,8 +412,8 @@ Await it from an action body or another imperative scope.

- Start the generation from a server function that returns at once; the job belongs to a module-level registry and writes the reply into a store as it arrives.
- Pass the job its own abort signal; the request's signal ends only the watcher.
- Deliver the reply through `live(GET(async function*))`, yielding the whole reply so far and the current reply first on every connection.
- Render it in the browser with `createProjection`, or on the server as a live server component mounted with `dynamicComponent` through `liveQuery(live(GET(fn)), key)`.
- Deliver the reply through `live(async function*)`, yielding the whole reply so far and the current reply first on every connection.
- Render it in the browser with `createProjection`, or on the server as a live server component mounted with `dynamicComponent` through `liveQuery(fn, key)`.
- Hold an optimistic row with `createOptimisticStore` inside an `action`, confirm it with `until` over the live thread's ids, and filter the row by the same ids.
- A reconnect sends `Last-Event-ID`; an unchanged first value is skipped, and partial replay needs a log the source reads the header against.

Expand Down
2 changes: 1 addition & 1 deletion src/routes/reference/(3)solid-router/(4)data.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ const messages = createMemo(() => roomMessages(props.room));
- Consumers of one key share one connection. A late consumer receives the latest value first. The connection closes when the last consumer leaves.
- A stream that has delivered a value and then fails with a network error or a 5xx status reconnects with exponential backoff. The latest value stays available meanwhile.
- A failure before the first value, or a 4xx status, ends the channel and throws to consumers.
- When `fn` is a server function without a declared method, `liveQuery` wraps it as a GET server function.
- When `fn` is a server function, `liveQuery` declares it with `live()`, which implies `GET`, so the connection gets the transport's live lifecycle. A function already declared with `live()` passes through unchanged.
- A call under preload intent connects the channel and holds it open for 5 seconds.
- Mutations do not revalidate live keys. `revalidate(key)` with `force` reconnects the matching channels.
- On the server, the document renders the first value. Hydration adopts it and reconnects.
Expand Down
Loading