@rsc-kit/mcp 0.27.1 → 0.28.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/dist/recipes.js +86 -16
- package/dist/recipes.js.map +1 -1
- package/guides/deployment.md +2 -2
- package/guides/go.md +1 -1
- package/guides/index.json +5 -0
- package/guides/laravel.md +25 -13
- package/guides/live-data.md +738 -0
- package/guides/sections.md +4 -460
- package/guides/where-it-runs.md +8 -0
- package/package.json +1 -1
package/dist/recipes.js
CHANGED
|
@@ -1826,17 +1826,20 @@ SAY IT CHANGED - wherever the change happens, not only in an action:
|
|
|
1826
1826
|
Laravel: Rsc::changed("team:$teamId:repos"); // a controller, a job, a listener
|
|
1827
1827
|
JS only: import { changed } from '@rsc-kit/core/changed'; await changed(\`team:\${team}:repos\`)
|
|
1828
1828
|
ANY DATABASE - rsc-kit ships no driver; hand it yours. Postgres:
|
|
1829
|
-
postgresVersions(sql) (postgres.js/Bun.sql)
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
sqlVersions({ query: async (
|
|
1829
|
+
postgresVersions(sql) (postgres.js/Bun.sql, or Drizzle's db.$client) - one
|
|
1830
|
+
statement per change, instant via LISTEN. MySQL/MariaDB: sqlVersions({
|
|
1831
|
+
dialect: 'mysql', query: async (t, p) => (await pool.query(t, p))[0] })
|
|
1832
|
+
(mysql2). SQLite: sqlVersions({ dialect: 'sqlite', query: async (t, p) =>
|
|
1833
|
+
db.query(t).all(...p) }). libSQL/Turso: sqlVersions({ dialect: 'sqlite',
|
|
1834
|
+
query: async (sql, args) => (await client.execute({ sql, args })).rows }).
|
|
1835
|
+
Always pass dialect: without it a change is a read + write per name.
|
|
1833
1836
|
Redis/ORM query builders: createVersions({ read, bump, listen?, notify? }).
|
|
1834
1837
|
Instant across instances: postgres.js LISTEN, Redis pub/sub; others within ~1s.
|
|
1835
1838
|
NO BACKEND (JS only) - changed() works as-is on one server. With more than
|
|
1836
1839
|
one instance, or a worker process that finishes jobs, keep versions in a
|
|
1837
1840
|
table they all share: installVersionSource(postgresVersions(sql)) in
|
|
1838
1841
|
instrumentation.ts register() AND in the worker (postgres.js LISTENs, so
|
|
1839
|
-
cross-instance changes are instant; Bun.sql
|
|
1842
|
+
cross-instance changes are instant; so does Bun.sql (Bun 1.4+); any SQL
|
|
1840
1843
|
driver/ORM: sqlVersions({ query }) - Prisma: query: (t, p) =>
|
|
1841
1844
|
prisma.$queryRawUnsafe(t, ...p); anything else (Drizzle query builder, Redis,
|
|
1842
1845
|
KV): createVersions({ read(names), bump(names), listen?, notify? }) and
|
|
@@ -1845,16 +1848,65 @@ backend's. In-memory versions fail with 2+ instances; in production rsc-kit logs
|
|
|
1845
1848
|
warning once when it falls back to them - installVersionSource(memoryVersions())
|
|
1846
1849
|
says "one instance on purpose" and silences it.
|
|
1847
1850
|
Table: rsc_versions(name TEXT PRIMARY KEY, version BIGINT NOT NULL) - same as
|
|
1848
|
-
Go's SQLVersions. ANY process can bump without rsc-kit
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
+
Go's SQLVersions. ANY process can bump without rsc-kit, with one upsert that
|
|
1852
|
+
moves version to GREATEST(version + 1, now in ms) - Postgres:
|
|
1853
|
+
INSERT ... VALUES (name, (EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint)
|
|
1854
|
+
ON CONFLICT (name) DO UPDATE SET version = GREATEST(rsc_versions.version + 1,
|
|
1855
|
+
EXCLUDED.version); then NOTIFY rsc_versions. MySQL: GREATEST(version + 1,
|
|
1856
|
+
VALUES(version)) with FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000); SQLite: MAX(...)
|
|
1857
|
+
with CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER).
|
|
1858
|
+
CLEANUP - a version is the time a name last changed, so deleting old rows is
|
|
1859
|
+
always safe (a tab holding one refreshes once; it never comes back at a value
|
|
1860
|
+
a tab holds): versions.prune() on sqlVersions/postgresVersions (30 days), or
|
|
1861
|
+
DELETE FROM rsc_versions WHERE version < <ms 30 days ago> from a cron.
|
|
1862
|
+
memoryVersions forgets after 30 days; the server forgets names no tab
|
|
1863
|
+
watches. Go: store.Prune(ctx, 0) on SQLVersions/MemoryVersions. Laravel:
|
|
1864
|
+
cache keys expire after RSC_VERSIONS_KEEP_DAYS (30); with
|
|
1865
|
+
RSC_VERSIONS=database schedule php artisan rsc:prune-versions daily. A custom store's bump should use nextVersion(current), not + 1.
|
|
1851
1866
|
TABLE - the app owns rsc_versions (rsc-kit never creates/alters it at
|
|
1852
1867
|
runtime): name TEXT PRIMARY KEY (MySQL: VARCHAR(255)), version BIGINT NOT
|
|
1853
1868
|
NULL. Extra columns fine if they have defaults. Drizzle: a pgTable in your
|
|
1854
1869
|
schema + createVersions with the query builder (not raw SQL).
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1870
|
+
POOLERS - a pooler in TRANSACTION mode (PgBouncer; DigitalOcean/Supabase/Neon
|
|
1871
|
+
pooled URLs) silently drops LISTEN. postgresVersions probes once as it starts
|
|
1872
|
+
listening; if deaf it logs why and reads every second. Fix:
|
|
1873
|
+
postgresVersions(pool, { listenWith: directOrSessionModeClient }) - one extra
|
|
1874
|
+
connection per process; reads/writes stay on the pool. (await connection() is
|
|
1875
|
+
unrelated: it is about rendering per request.)
|
|
1876
|
+
POLL ON PURPOSE - postgresVersions(pool, { listen: false }) when direct
|
|
1877
|
+
connections are scarce: no listener, no probe, no warning; one indexed read per
|
|
1878
|
+
second per process for all its tabs; other instances' changes within ~1s.
|
|
1879
|
+
WORKERS / SERVERLESS - the stream is stateless (signed names) so it runs where
|
|
1880
|
+
responses stream, but: each isolate/instance watches for its own tabs, so
|
|
1881
|
+
in-memory versions are useless - use a shared store. Platform request limits
|
|
1882
|
+
cut streams (they reconnect and catch up); billed-per-second functions pay for
|
|
1883
|
+
every watching tab; Hyperdrive/serverless drivers can't LISTEN (probe falls back
|
|
1884
|
+
to 1s, or listen: false). No streaming at all: usePolling.
|
|
1885
|
+
CLOUDFLARE WORKERS + D1 (verified on wrangler dev; examples/cloudflare /live):
|
|
1886
|
+
// src/versions.ts - import the binding INSIDE the query: the build prerenders
|
|
1887
|
+
// in Node, runs register(), and cannot load 'cloudflare:workers' (a top-level
|
|
1888
|
+
// import breaks the build).
|
|
1889
|
+
const db = async () => (await import('cloudflare:workers')).env.DB
|
|
1890
|
+
export const versions = () => sqlVersions({ dialect: 'sqlite',
|
|
1891
|
+
query: async (t, p) => (await (await db()).prepare(t).bind(...p).all()).results })
|
|
1892
|
+
// src/instrumentation.ts: installVersionSource(versions)
|
|
1893
|
+
// wrangler.jsonc (Nitro merges it): "d1_databases": [{ "binding": "DB",
|
|
1894
|
+
// "database_name": "my-app", "database_id": "<id>", "migrations_dir": "migrations" }]
|
|
1895
|
+
// migrations/0001_rsc_versions.sql:
|
|
1896
|
+
// CREATE TABLE IF NOT EXISTS rsc_versions (name TEXT PRIMARY KEY, version BIGINT NOT NULL);
|
|
1897
|
+
// npx wrangler d1 migrations apply DB --remote (--local for wrangler dev)
|
|
1898
|
+
// npx wrangler secret put RSC_SIGNING_SECRET (.dev.vars for wrangler dev)
|
|
1899
|
+
// A section reading D1 itself: await connection() first - D1 is per request.
|
|
1900
|
+
INSTALL LAZILY - installVersionSource(() => postgresVersions(client)): a
|
|
1901
|
+
factory, made on first use. register() also runs in the build, often without
|
|
1902
|
+
DB settings; a store made there makes its client there.
|
|
1903
|
+
LISTEN THROUGH AN ORM - pass the driver's listen/notify to createVersions:
|
|
1904
|
+
Drizzle on Bun.sql: listen: wake => client.listen('rsc_versions', wake),
|
|
1905
|
+
notify: () => client.notify('rsc_versions', '') with the SQL client you gave
|
|
1906
|
+
drizzle(). node-postgres: a dedicated pg.Client, LISTEN rsc_versions, and
|
|
1907
|
+
on('notification', wake). A listening store reads the table only as a safety
|
|
1908
|
+
net (~every 5s per server), not every second. NOTIFY inside a transaction is
|
|
1909
|
+
delivered on commit, so it keeps the after-commit rule for you.
|
|
1858
1910
|
AFTER COMMIT - call changed() AFTER the transaction resolves, never inside
|
|
1859
1911
|
it: a tab refreshes at once and would read the old data, then sit at the new
|
|
1860
1912
|
version. Laravel: DB::afterCommit(fn () => Rsc::changed(...)).
|
|
@@ -1867,15 +1919,33 @@ request.
|
|
|
1867
1919
|
SECRET - set RSC_SIGNING_SECRET, the same on every instance, with or without
|
|
1868
1920
|
a backend (NOT the host-call secret). In production an app using refreshOn
|
|
1869
1921
|
refuses to serve without it. Names are signed per request, never at build.
|
|
1870
|
-
REQUEST - a refreshOn function
|
|
1871
|
-
|
|
1922
|
+
REQUEST - a refreshOn function gets params and searchParams ALREADY AWAITED
|
|
1923
|
+
(({ params }) => [\`team:\${params.team}\`] is right), and runs per request:
|
|
1924
|
+
cookies()/headers() work, e.g. a name for the signed-in user.
|
|
1925
|
+
ISOLATION - a name is a signal, not data or a permission: a refresh renders
|
|
1926
|
+
for that tab's visitor through their own session and guards, so nobody sees
|
|
1927
|
+
another's data through a name. A tab can only listen for names its page was
|
|
1928
|
+
rendered with (signed). Name the data narrowly with ids: conversation:{id},
|
|
1929
|
+
inbox:{userId}. No emails/secrets in names. Never shared: true on per-visitor
|
|
1930
|
+
sections.
|
|
1872
1931
|
SHARED - section(name, C, { refreshOn, shared: true }) for a section that
|
|
1873
1932
|
renders the same for everyone allowed to see the page: tabs refreshing
|
|
1874
1933
|
because of the same change share ONE render (guards still run per tab).
|
|
1875
1934
|
Never on per-visitor content - rpc() runs with the first visitor's session.
|
|
1876
|
-
LARAVEL
|
|
1877
|
-
|
|
1878
|
-
|
|
1935
|
+
LARAVEL - keep the default (versions in the cache; the renderer asks Laravel
|
|
1936
|
+
~every 2s, ONE request per renderer process for all its tabs - negligible).
|
|
1937
|
+
Laravel stays the only thing talking to its database. RSC_VERSIONS=database
|
|
1938
|
+
(publish rsc-migrations) is for pruning or for writers outside Laravel; Laravel
|
|
1939
|
+
still answers. INSTANT: Laravel broadcasting (any Pusher-protocol server:
|
|
1940
|
+
Reverb, Pusher, Soketi) - RSC_BROADCAST=true in Laravel, RSC_BROADCAST_URL +
|
|
1941
|
+
RSC_BROADCAST_KEY for the renderer. Rsc::changed() announces a change (no
|
|
1942
|
+
names - public channel), the renderer asks Laravel at once, else every 30s.
|
|
1943
|
+
Never have the renderer read Laravel's database.
|
|
1944
|
+
WAKE ANYTHING - wakeOn(listen) from @rsc-kit/core/changed: listenToBroadcast({
|
|
1945
|
+
url, key }), or (wake) => subscriber.subscribe('rsc:changed', wake) for Redis.
|
|
1946
|
+
A wake carries nothing; the renderer then asks where versions live. Versions
|
|
1947
|
+
are still needed: they say WHICH names moved, per visitor, and let a
|
|
1948
|
+
reconnecting tab catch up.
|
|
1879
1949
|
DEBUG - dev console lists what each region watches; a region missing there
|
|
1880
1950
|
rendered no names (server log says why). Hidden tabs stop watching and catch
|
|
1881
1951
|
up when shown.
|