@rsc-kit/core 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.
@@ -0,0 +1,22 @@
1
+ /** Where the broadcast server is, and which channel and event mean "a version moved". */
2
+ export interface BroadcastOptions {
3
+ /** The server: `wss://ws.example.com` or `http://localhost:8080` (http is read as ws). */
4
+ url: string;
5
+ /** The app key the server knows this app by (Laravel: REVERB_APP_KEY, or PUSHER_APP_KEY). */
6
+ key: string;
7
+ /** Default `rsc-versions`, the channel Laravel's Rsc::changed() broadcasts on. */
8
+ channel?: string;
9
+ /** Default `rsc.changed`, the event it broadcasts. */
10
+ event?: string;
11
+ }
12
+ /**
13
+ * A `wakeOn` listener for a broadcast server:
14
+ *
15
+ * wakeOn(listenToBroadcast({ url: 'wss://ws.example.com', key: process.env.REVERB_APP_KEY! }))
16
+ *
17
+ * or, with no code, `RSC_BROADCAST_URL` and `RSC_BROADCAST_KEY` in the
18
+ * renderer's environment. Uses the runtime's own WebSocket - Bun, Node 22,
19
+ * Deno - and reconnects with backoff when the connection drops, waking once
20
+ * it is back, since anything announced while it was gone was lost.
21
+ */
22
+ export declare function listenToBroadcast(options: BroadcastOptions): (wake: () => void) => () => void;
@@ -0,0 +1,109 @@
1
+ // Waking on a broadcast: the renderer subscribes to one channel on a
2
+ // broadcast server - one speaking the Pusher protocol, which Laravel's
3
+ // Reverb, Pusher and Soketi all do - and is woken whenever a change is
4
+ // announced on it.
5
+ //
6
+ // The announcement carries nothing: no names, no data. Anyone may subscribe
7
+ // to a public channel, so what goes over it must be safe for anyone to see;
8
+ // "something changed" is. The renderer then asks wherever the versions live
9
+ // - the backend, through __rsc.changed - which answers for its visitor's
10
+ // names alone, as it always has.
11
+ const PING_MS = 60_000;
12
+ /**
13
+ * A `wakeOn` listener for a broadcast server:
14
+ *
15
+ * wakeOn(listenToBroadcast({ url: 'wss://ws.example.com', key: process.env.REVERB_APP_KEY! }))
16
+ *
17
+ * or, with no code, `RSC_BROADCAST_URL` and `RSC_BROADCAST_KEY` in the
18
+ * renderer's environment. Uses the runtime's own WebSocket - Bun, Node 22,
19
+ * Deno - and reconnects with backoff when the connection drops, waking once
20
+ * it is back, since anything announced while it was gone was lost.
21
+ */
22
+ export function listenToBroadcast(options) {
23
+ const channel = options.channel ?? "rsc-versions";
24
+ const event = options.event ?? "rsc.changed";
25
+ const base = options.url.replace(/^http/, "ws").replace(/\/+$/, "");
26
+ const url = `${base}/app/${encodeURIComponent(options.key)}?protocol=7&client=rsc-kit&version=1&flash=false`;
27
+ return (wake) => {
28
+ let socket = null;
29
+ let stopped = false;
30
+ let failures = 0;
31
+ let retry;
32
+ let ping;
33
+ let warned = false;
34
+ const send = (message) => {
35
+ try {
36
+ socket?.send(JSON.stringify(message));
37
+ }
38
+ catch {
39
+ // Closing; the close handler reconnects.
40
+ }
41
+ };
42
+ const connect = () => {
43
+ if (stopped)
44
+ return;
45
+ const WebSocketImpl = globalThis
46
+ .WebSocket;
47
+ if (!WebSocketImpl) {
48
+ console.warn("[rsc-kit] listenToBroadcast needs a runtime with WebSocket (Bun, Node 22+): not listening.");
49
+ return;
50
+ }
51
+ const ws = new WebSocketImpl(url);
52
+ socket = ws;
53
+ ws.onmessage = (message) => {
54
+ let frame;
55
+ try {
56
+ frame = JSON.parse(String(message.data));
57
+ }
58
+ catch {
59
+ return;
60
+ }
61
+ if (frame.event === "pusher:connection_established") {
62
+ send({ event: "pusher:subscribe", data: { channel } });
63
+ }
64
+ else if (frame.event === "pusher_internal:subscription_succeeded") {
65
+ failures = 0;
66
+ // Connected again after a drop: whatever was announced meanwhile is
67
+ // gone, so ask now.
68
+ wake();
69
+ }
70
+ else if (frame.event === "pusher:ping") {
71
+ send({ event: "pusher:pong", data: {} });
72
+ }
73
+ else if (frame.event === "pusher:error") {
74
+ if (!warned) {
75
+ warned = true;
76
+ console.warn("[rsc-kit] the broadcast server refused: " +
77
+ JSON.stringify(frame.data));
78
+ }
79
+ }
80
+ else if (frame.event === event && frame.channel === channel) {
81
+ wake();
82
+ }
83
+ };
84
+ ws.onclose = () => {
85
+ clearInterval(ping);
86
+ if (socket === ws)
87
+ socket = null;
88
+ if (stopped)
89
+ return;
90
+ retry = setTimeout(connect, Math.min(30_000, 1000 * 2 ** failures++));
91
+ retry.unref?.();
92
+ };
93
+ ws.onerror = () => {
94
+ // The close that follows reconnects.
95
+ };
96
+ // Some proxies drop a connection that only listens.
97
+ ping = setInterval(() => send({ event: "pusher:ping", data: {} }), PING_MS);
98
+ ping.unref?.();
99
+ };
100
+ connect();
101
+ return () => {
102
+ stopped = true;
103
+ clearTimeout(retry);
104
+ clearInterval(ping);
105
+ socket?.close();
106
+ };
107
+ };
108
+ }
109
+ //# sourceMappingURL=broadcast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broadcast.js","sourceRoot":"","sources":["../src/broadcast.ts"],"names":[],"mappings":"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","sourcesContent":["// Waking on a broadcast: the renderer subscribes to one channel on a\n// broadcast server - one speaking the Pusher protocol, which Laravel's\n// Reverb, Pusher and Soketi all do - and is woken whenever a change is\n// announced on it.\n//\n// The announcement carries nothing: no names, no data. Anyone may subscribe\n// to a public channel, so what goes over it must be safe for anyone to see;\n// \"something changed\" is. The renderer then asks wherever the versions live\n// - the backend, through __rsc.changed - which answers for its visitor's\n// names alone, as it always has.\n\n/** Where the broadcast server is, and which channel and event mean \"a version moved\". */\nexport interface BroadcastOptions {\n /** The server: `wss://ws.example.com` or `http://localhost:8080` (http is read as ws). */\n url: string;\n /** The app key the server knows this app by (Laravel: REVERB_APP_KEY, or PUSHER_APP_KEY). */\n key: string;\n /** Default `rsc-versions`, the channel Laravel's Rsc::changed() broadcasts on. */\n channel?: string;\n /** Default `rsc.changed`, the event it broadcasts. */\n event?: string;\n}\n\nconst PING_MS = 60_000;\n\n/**\n * A `wakeOn` listener for a broadcast server:\n *\n * wakeOn(listenToBroadcast({ url: 'wss://ws.example.com', key: process.env.REVERB_APP_KEY! }))\n *\n * or, with no code, `RSC_BROADCAST_URL` and `RSC_BROADCAST_KEY` in the\n * renderer's environment. Uses the runtime's own WebSocket - Bun, Node 22,\n * Deno - and reconnects with backoff when the connection drops, waking once\n * it is back, since anything announced while it was gone was lost.\n */\nexport function listenToBroadcast(\n options: BroadcastOptions,\n): (wake: () => void) => () => void {\n const channel = options.channel ?? \"rsc-versions\";\n const event = options.event ?? \"rsc.changed\";\n const base = options.url.replace(/^http/, \"ws\").replace(/\\/+$/, \"\");\n const url = `${base}/app/${encodeURIComponent(options.key)}?protocol=7&client=rsc-kit&version=1&flash=false`;\n\n return (wake) => {\n let socket: WebSocket | null = null;\n let stopped = false;\n let failures = 0;\n let retry: ReturnType<typeof setTimeout> | undefined;\n let ping: ReturnType<typeof setInterval> | undefined;\n let warned = false;\n\n const send = (message: unknown) => {\n try {\n socket?.send(JSON.stringify(message));\n } catch {\n // Closing; the close handler reconnects.\n }\n };\n\n const connect = () => {\n if (stopped) return;\n\n const WebSocketImpl = (globalThis as { WebSocket?: typeof WebSocket })\n .WebSocket;\n\n if (!WebSocketImpl) {\n console.warn(\n \"[rsc-kit] listenToBroadcast needs a runtime with WebSocket (Bun, Node 22+): not listening.\",\n );\n return;\n }\n\n const ws = new WebSocketImpl(url);\n\n socket = ws;\n\n ws.onmessage = (message: MessageEvent) => {\n let frame: { event?: string; channel?: string; data?: unknown };\n\n try {\n frame = JSON.parse(String(message.data)) as typeof frame;\n } catch {\n return;\n }\n\n if (frame.event === \"pusher:connection_established\") {\n send({ event: \"pusher:subscribe\", data: { channel } });\n } else if (frame.event === \"pusher_internal:subscription_succeeded\") {\n failures = 0;\n // Connected again after a drop: whatever was announced meanwhile is\n // gone, so ask now.\n wake();\n } else if (frame.event === \"pusher:ping\") {\n send({ event: \"pusher:pong\", data: {} });\n } else if (frame.event === \"pusher:error\") {\n if (!warned) {\n warned = true;\n console.warn(\n \"[rsc-kit] the broadcast server refused: \" +\n JSON.stringify(frame.data),\n );\n }\n } else if (frame.event === event && frame.channel === channel) {\n wake();\n }\n };\n\n ws.onclose = () => {\n clearInterval(ping);\n if (socket === ws) socket = null;\n if (stopped) return;\n\n retry = setTimeout(connect, Math.min(30_000, 1000 * 2 ** failures++));\n (retry as { unref?: () => void }).unref?.();\n };\n\n ws.onerror = () => {\n // The close that follows reconnects.\n };\n\n // Some proxies drop a connection that only listens.\n ping = setInterval(\n () => send({ event: \"pusher:ping\", data: {} }),\n PING_MS,\n );\n (ping as { unref?: () => void }).unref?.();\n };\n\n connect();\n\n return () => {\n stopped = true;\n clearTimeout(retry);\n clearInterval(ping);\n socket?.close();\n };\n };\n}\n"]}
package/dist/changed.d.ts CHANGED
@@ -26,6 +26,7 @@
26
26
  * answers at once, and the watcher asks again on its interval. The protocol
27
27
  * is the same either way, and so is the page.
28
28
  */
29
+ export { listenToBroadcast, type BroadcastOptions } from "./broadcast.js";
29
30
  /** The reserved name a backend answers name versions on. */
30
31
  export declare const CHANGED_FUNCTION = "__rsc.changed";
31
32
  /** What `__rsc.changed` is asked. */
@@ -52,8 +53,33 @@ export interface VersionSource {
52
53
  /** Say these names changed. Only a source that holds its own versions can. */
53
54
  bump(names: string[]): void | Promise<void>;
54
55
  }
55
- /** Versions kept in this process: the source without a backend, and the one tests use. */
56
- export declare function memoryVersions(): VersionSource;
56
+ /**
57
+ * The version a name moves to: the larger of one past where it was and the
58
+ * current time in milliseconds.
59
+ *
60
+ * A counter would do for "it moved", but a counter repeats once its row is
61
+ * gone - deleted to keep the store small, a name starts again from 0 and
62
+ * climbs back to a value some tab is still holding, and that tab misses the
63
+ * change. A time never comes round again, so a name may be forgotten at any
64
+ * moment: a tab holding the old value sees a different one, and at worst
65
+ * refreshes once for nothing. It is also when the name last moved, which is
66
+ * all cleanup needs. Every store bumps with it; one that writes `+ 1` still
67
+ * works, it is just not safe to prune.
68
+ */
69
+ export declare function nextVersion(current?: number): number;
70
+ /** The safety-net read of a store that listens: longer than any ask waits, so once per ask. */
71
+ export declare const LISTENING_POLL_MS = 30000;
72
+ /** How long a name nobody changes is kept, by the stores that forget on their own. */
73
+ export declare const VERSIONS_KEPT_MS: number;
74
+ /**
75
+ * Versions kept in this process: the source without a backend, and the one
76
+ * tests use. A name not changed in `forgetAfter` ms (30 days) is forgotten,
77
+ * swept at most once an hour, so a process up for months does not hold every
78
+ * name it ever saw.
79
+ */
80
+ export declare function memoryVersions(options?: {
81
+ forgetAfter?: number;
82
+ }): VersionSource;
57
83
  type HostFn = (name: string, ...args: unknown[]) => Promise<unknown>;
58
84
  /**
59
85
  * Versions kept by the backend, read through `__rsc.changed`.
@@ -67,6 +93,17 @@ export declare function backendVersions(call: HostFn): VersionSource;
67
93
  /** Runs one statement with positional parameters and resolves to its rows. */
68
94
  export type SqlQuery = (text: string, params: unknown[]) => Promise<readonly Record<string, unknown>[]>;
69
95
  export interface SqlVersionsOptions {
96
+ /**
97
+ * Which database, so a change is one statement - an upsert of every name
98
+ * it moves - rather than a read and a write per name. Left out, the store
99
+ * uses only SQL every database has, at the cost of those extra queries.
100
+ */
101
+ dialect?: "postgres" | "mysql" | "sqlite";
102
+ /**
103
+ * Postgres only: NOTIFY this channel in the same statement as the upsert,
104
+ * so a listening server wakes - and only once the write commits.
105
+ */
106
+ channel?: string;
70
107
  /**
71
108
  * Your driver, as a function of SQL text and parameters:
72
109
  *
@@ -93,26 +130,23 @@ export interface SqlVersionsOptions {
93
130
  * notify: () => sql.notify('rsc_versions', '')
94
131
  */
95
132
  notify?: () => unknown;
96
- /** How often a waiting ask reads the table. Default 1000ms. */
133
+ /**
134
+ * How often a waiting ask reads the table: every second by default, or,
135
+ * with `listen`, only as a safety net - about once per ask.
136
+ */
97
137
  poll?: number;
98
138
  }
99
- /**
100
- * Versions in a table every instance shares: the web servers, and the worker
101
- * that finishes a job and says so. The same table and columns Go's
102
- * `SQLVersions` uses, so a Go service and a JavaScript worker can share one.
103
- *
104
- * installVersionSource(sqlVersions({
105
- * query: (text, params) => sql.unsafe(text, params),
106
- * placeholder: (n) => '$' + n,
107
- * listen: (wake) => sql.listen('rsc_versions', wake),
108
- * notify: () => sql.notify('rsc_versions', ''),
109
- * }))
110
- *
111
- * Written with an UPDATE and, the first time, an INSERT - two statements any
112
- * SQL dialect has - rather than an upsert each spells differently. Takes the
113
- * app's own driver, so the engine has no database dependency of its own.
114
- */
115
- export declare function sqlVersions(options: SqlVersionsOptions): VersionSource;
139
+ /** A store whose old names can be cleared out: the SQL ones. */
140
+ export interface PrunableVersions extends VersionSource {
141
+ /**
142
+ * Delete every name not changed in `olderThan` ms (30 days by default).
143
+ * Always safe: a version is a time and never comes round again, so a tab
144
+ * still holding a pruned name sees it differ and refreshes once. Run it
145
+ * from a scheduled job - a daily one is plenty.
146
+ */
147
+ prune(olderThan?: number): Promise<void>;
148
+ }
149
+ export declare function sqlVersions(options: SqlVersionsOptions): PrunableVersions;
116
150
  /**
117
151
  * Where versions are kept, as the app writes it for its own database: the
118
152
  * three things rsc-kit cannot know about your storage, and nothing else.
@@ -134,7 +168,13 @@ export declare function sqlVersions(options: SqlVersionsOptions): VersionSource;
134
168
  export interface VersionStore {
135
169
  /** The current version of each name asked for. A name it has never seen may be left out: it is 0. */
136
170
  read(names: string[]): Promise<Record<string, number>>;
137
- /** Move each name's version - by one, or to anything new: watchers compare, they do not count. */
171
+ /**
172
+ * Move each name's version to `nextVersion(current)` - one past where it
173
+ * was, or the current time in ms if that is larger. Watchers only compare,
174
+ * so any new value works; this one never repeats, which is what makes it
175
+ * safe to delete a name later. A plain `+ 1` works too, until a row is
176
+ * deleted.
177
+ */
138
178
  bump(names: string[]): Promise<void>;
139
179
  /**
140
180
  * Called once with a function that wakes every waiting ask, when the store
@@ -172,9 +212,10 @@ export interface PostgresClient {
172
212
  * installVersionSource(postgresVersions(sql))
173
213
  *
174
214
  * `sqlVersions` with Postgres's placeholders, and - when the client can
175
- * LISTEN, as postgres.js can - a change on one instance wakes the waiting asks
176
- * on every other at once, rather than on their next read of the table.
177
- * Without it (`Bun.sql`), the table is read once a second while an ask waits.
215
+ * LISTEN, as postgres.js and `Bun.sql` (Bun 1.4+) can - a change on one
216
+ * instance wakes the waiting asks on every other at once, rather than on
217
+ * their next read of the table. Without it, the table is read once a second
218
+ * while an ask waits.
178
219
  *
179
220
  * CREATE TABLE rsc_versions (name TEXT PRIMARY KEY, version BIGINT NOT NULL)
180
221
  */
@@ -182,7 +223,7 @@ export declare function postgresVersions(sql: PostgresClient, options?: {
182
223
  table?: string;
183
224
  channel?: string;
184
225
  poll?: number;
185
- }): VersionSource;
226
+ }): PrunableVersions;
186
227
  /**
187
228
  * Install where versions are read from - a table, Redis, any store - for this
188
229
  * process; `null` takes it out again.
@@ -244,6 +285,20 @@ export type SignedName = [version: number, signature: string];
244
285
  export declare const CHANGES_INTERVAL_MS = 2000;
245
286
  /** How long the source may hold one ask. Bounds how long a new tab waits to join the shared ask. */
246
287
  export declare const CHANGES_WAIT_MS = 5000;
288
+ /**
289
+ * Wake the renderer when a version may have moved, so it asks then rather
290
+ * than on its next interval.
291
+ *
292
+ * wakeOn(listenToBroadcast({ url, key })) // a broadcast server
293
+ * wakeOn((wake) => subscriber.subscribe('rsc', wake)) // a Redis channel
294
+ *
295
+ * `listen` is given a function to call - it carries no names and no data;
296
+ * the renderer asks wherever the versions live, as always. It is started
297
+ * when the first tab starts watching, never in a build. With one installed,
298
+ * a backend that answers at once is asked again every 30 seconds as a safety
299
+ * net instead of every two.
300
+ */
301
+ export declare function wakeOn(listen: (wake: () => void) => unknown): void;
247
302
  /**
248
303
  * The stream a tab watches its names on: `GET /_rsc/changes?w=[[name, version, signature], …]`.
249
304
  *
@@ -254,7 +309,8 @@ export declare const CHANGES_WAIT_MS = 5000;
254
309
  */
255
310
  export declare function changes(request: Request): Promise<Response>;
256
311
  /** For tests: forget every stream and version this process has seen. */
312
+ /** For tests: how many names this process remembers for its open streams. */
313
+ export declare function rememberedNames(): number;
257
314
  export declare function resetChanges(): void;
258
315
  /** The url a tab opens to watch, for the client. */
259
316
  export declare const CHANGES_PATH: "/_rsc/changes";
260
- export {};