@rsc-kit/mcp 0.27.0 → 0.28.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.
@@ -215,8 +215,10 @@ export const GET = events(async function* ({ params, signal }) {
215
215
 
216
216
  An ordinary `route.ts`, so everything above applies: it lives beside its
217
217
  pages, runs the `middleware.ts` above it, takes the same params. `events()`
218
- frames each yield as JSON, sends a keepalive so a proxy does not drop an idle
219
- stream, sets `text/event-stream` and `no-store`, and ends the generator when
218
+ frames each yield as JSON, sends a keepalive every eight seconds so nothing in
219
+ front drops an idle stream ([`RSC_STREAM_KEEPALIVE_MS`](/hosts/deployment#streams-and-idle-timeouts)
220
+ to change it), sets `text/event-stream` and `no-store, no-transform`, is never
221
+ compressed, and ends the generator when
220
222
  the browser goes away — that is what `signal` is for. `named(event, data, {
221
223
  id })` names a message or gives it an id, for a client that listens by name or
222
224
  resumes after a reconnect.
@@ -160,6 +160,75 @@ names the stylesheet and the entry. Nothing to configure, and nothing it
160
160
  replaces: the stylesheet still blocks paint, and inlining critical CSS
161
161
  stacks on top of it.
162
162
 
163
+ ## Streams and idle timeouts
164
+
165
+ An `events()` route and the [`refreshOn`](/guides/sections#refreshing-on-a-change-from-outside)
166
+ stream hold a connection open and are quiet most of the time. Anything in
167
+ front that closes a quiet connection - the server's idle timeout, a proxy's
168
+ read timeout - would cut them, so each sends a keepalive line every **eight
169
+ seconds**: under Bun's default ten, and well under nginx's sixty and
170
+ Cloudflare's hundred.
171
+
172
+ If something in front closes idle connections sooner, set the keepalive
173
+ below it:
174
+
175
+ ```sh
176
+ RSC_STREAM_KEEPALIVE_MS=4000 # under a 5-second idle timeout
177
+ ```
178
+
179
+ Too long a keepalive does not lose anything - the browser reconnects, and a
180
+ tab catches up on whatever moved - but every open tab is cut and reopens each
181
+ time the timeout passes. Both streams are sent with `Cache-Control: no-store,
182
+ no-transform` and are never compressed, so nothing in front buffers them.
183
+
184
+ ## Secrets
185
+
186
+ rsc-kit reads up to three. Each is its own key for its own job - never reuse
187
+ one as another, so one leaking does not hand out the rest.
188
+
189
+ | Variable | What it protects | Who needs it | Read by |
190
+ | --- | --- | --- | --- |
191
+ | `RSC_HOST_CALL_SECRET` | calls between the renderer and a Go or Laravel backend | an app with a backend - required | the renderer **and** the backend |
192
+ | `RSC_SIGNING_SECRET` | the names a page hands a tab for [`refreshOn`](/guides/sections#refreshing-on-a-change-from-outside) | an app that uses `refreshOn` - required in production | the renderer |
193
+ | `RSC_ACTION_ENCRYPTION_KEY` | values a server action sends through the browser, [across a deploy](#server-actions-across-a-deploy) | optional | the renderer, from the build |
194
+
195
+ `bun create rsc-kit` and `rsc-kit init` generate the first two into `.env` for
196
+ an app with a backend, and `php artisan rsc:install` does the same for
197
+ Laravel. That is development. For production, generate each once and keep it
198
+ where you keep secrets - your platform's environment settings, not the repo:
199
+
200
+ ```sh
201
+ openssl rand -base64 32
202
+ ```
203
+
204
+ or, without openssl:
205
+
206
+ ```sh
207
+ node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"
208
+ php -r "echo base64_encode(random_bytes(32)), PHP_EOL;"
209
+ ```
210
+
211
+ Three rules apply to all of them:
212
+
213
+ - **The same value on every instance.** Each is checked by a process that may
214
+ not be the one that issued it: a tab served by one renderer, a call landing
215
+ on another backend replica. A value per instance fails for some requests and
216
+ not others.
217
+ - **At runtime, from the environment.** The built server reads them when it
218
+ starts, so one build deploys anywhere. `RSC_ACTION_ENCRYPTION_KEY` is the
219
+ exception: set it at build time too.
220
+ - **Never in git.** The scaffold's `.gitignore` already leaves out `.env`.
221
+
222
+ **Rotating one** is a deploy, and each says so differently:
223
+
224
+ - `RSC_HOST_CALL_SECRET` - the renderer and the backend must change together.
225
+ Between the two, every `rpc()` is refused, so deploy both at once.
226
+ - `RSC_SIGNING_SECRET` - pages already open hold names signed with the old
227
+ key; their watching is refused and they stop refreshing until reloaded.
228
+ Nothing else breaks.
229
+ - `RSC_ACTION_ENCRYPTION_KEY` - an action called from a page loaded before the
230
+ change fails, the same window as a deploy without the key pinned.
231
+
163
232
  ## Pages across a deploy
164
233
 
165
234
  A page from the previous build asks for a payload from the new one, whose
package/guides/go.md CHANGED
@@ -34,7 +34,7 @@ server in production:
34
34
 
35
35
  ```ini title=".env"
36
36
  RSC_BACKEND=http://127.0.0.1:8080
37
- RSC_HOST_CALL_SECRET=a-long-random-string
37
+ RSC_HOST_CALL_SECRET=… # openssl rand -base64 32 - see Secrets under Deployment
38
38
  ```
39
39
 
40
40
  And the endpoint, on whatever mux you already have:
@@ -285,7 +285,8 @@ reg.Names(&rsckit.SQLTags{DB: db, Placeholder: rsckit.Dollar}) // Postgres; ni
285
285
  ```
286
286
 
287
287
  Anything with a `Bump` and a `Versions` fits. Set `RSC_SIGNING_SECRET` for the renderer, the same on every instance:
288
- it signs the names a page hands the tab, and is not the host-call secret. [Sections](/guides/sections#refreshing-on-a-change-from-outside)
288
+ it signs the names a page hands the tab, and is not the host-call secret.
289
+ Generating and rotating both: [Secrets](/hosts/deployment#secrets). [Sections](/guides/sections#refreshing-on-a-change-from-outside)
289
290
  has the whole picture.
290
291
 
291
292
  ## Production
package/guides/laravel.md CHANGED
@@ -356,29 +356,42 @@ server's. PHP answers the renderer's ask at once rather than holding it, so
356
356
  a change reaches the tab on the renderer's next ask, a couple of seconds
357
357
  later. Set `RSC_SIGNING_SECRET` for the renderer, the same on every instance:
358
358
  it signs the names a page hands the tab, and is not the host-call secret.
359
+ Generating and rotating both: [Secrets](/hosts/deployment#secrets).
359
360
 
360
- **Watching without PHP.** By default the renderer asks PHP which versions
361
- moved, every couple of seconds while any tab is watching. Keep the versions in
362
- the `rsc_versions` table instead and the renderer reads them itself:
361
+ **What watching costs PHP.** The renderer asks Laravel which versions moved
362
+ once for every tab it serves, not once per tab: one request about every two
363
+ seconds per renderer process while any tab is watching, each a cache read.
364
+ Three renderers are one or two small requests a second for the whole fleet.
365
+ That is the default and the recommendation - Laravel stays the only thing
366
+ that talks to its database.
367
+
368
+ **Keeping versions in a table** (`RSC_VERSIONS=database`) is for when you
369
+ want them in the database rather than the cache: pruned on a schedule with
370
+ `rsc:prune-versions`, or written by something outside Laravel - a Go service,
371
+ a worker in another language - through the [shared table](/guides/sections#the-table).
372
+ Laravel still answers the renderer from it; nothing else changes.
363
373
 
364
374
  ```sh
365
375
  php artisan vendor:publish --tag=rsc-migrations && php artisan migrate
366
376
  RSC_VERSIONS=database
367
377
  ```
368
378
 
369
- ```ts title="src/instrumentation.ts"
370
- import { installVersionSource, postgresVersions } from '@rsc-kit/core/changed'
371
- import postgres from 'postgres'
379
+ **Instant, with broadcasting.** If the app has Laravel's broadcasting set up
380
+ (`php artisan install:broadcasting`) with a server that speaks the Pusher
381
+ protocol - Reverb, Pusher, Soketi - each `Rsc::changed()` can announce itself,
382
+ and the renderer asks the moment it hears:
372
383
 
373
- export function register() {
374
- installVersionSource(postgresVersions(postgres(process.env.DATABASE_URL!)))
375
- }
384
+ ```sh
385
+ RSC_BROADCAST=true # Laravel: announce every change
386
+ RSC_BROADCAST_URL=wss://ws.example.com # the renderer: where to listen
387
+ RSC_BROADCAST_KEY=your-app-key # the renderer: the server's app key
376
388
  ```
377
389
 
378
- Watching then costs PHP no requests, and on Postgres `Rsc::changed()` sends a
379
- NOTIFY, so tabs hear it at once. On MySQL use `sqlVersions` with your driver;
380
- the renderer reads the table once a second. [Sections](/guides/sections#refreshing-on-a-change-from-outside) has the
381
- whole picture.
390
+ A change then reaches the tabs at once, and between changes the renderer asks
391
+ only every thirty seconds, as a safety net. The announcement carries nothing -
392
+ the channel is public, and names may be one visitor's - so Laravel still
393
+ answers which names moved, and the renderer never touches the database. It is
394
+ sent after the transaction commits.
382
395
 
383
396
  ## Route middleware
384
397