@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 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). MySQL/MariaDB: sqlVersions({
1830
- query: async (t, p) => (await pool.query(t, p))[0] }) (mysql2). SQLite:
1831
- sqlVersions({ query: async (t, p) => db.query(t).all(...p) }). libSQL/Turso:
1832
- sqlVersions({ query: async (sql, args) => (await client.execute({ sql, args })).rows }).
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 polls once a second; any 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: upsert version + 1
1849
- (Postgres: ON CONFLICT (name) DO UPDATE SET version = rsc_versions.version + 1;
1850
- then NOTIFY rsc_versions).
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
- BUN / no LISTEN - keep Bun.sql (or Drizzle over it) for read/bump and open one
1856
- postgres.js connection ({ max: 1 }) only for listen/notify in createVersions:
1857
- instant across instances.
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 runs per request: cookies()/headers() work,
1871
- e.g. a name for the signed-in user.
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 AT SCALE - RSC_VERSIONS=database (publish rsc-migrations) and
1877
- installVersionSource(postgresVersions(sql)) in the renderer: watching costs
1878
- PHP zero requests; Postgres NOTIFY makes it instant.
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.