@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.
- package/dist/broadcast.d.ts +22 -0
- package/dist/broadcast.js +109 -0
- package/dist/broadcast.js.map +1 -0
- package/dist/changed.d.ts +82 -26
- package/dist/changed.js +252 -26
- package/dist/changed.js.map +1 -1
- package/dist/compress.js +8 -1
- package/dist/compress.js.map +1 -1
- package/dist/events.d.ts +21 -3
- package/dist/events.js +31 -6
- package/dist/events.js.map +1 -1
- package/dist/js/refreshOn.d.ts +9 -1
- package/dist/js/refreshOn.js +12 -1
- package/dist/js/refreshOn.js.map +1 -1
- package/dist/testHost.d.ts +15 -4
- package/dist/testHost.js +2 -11
- package/dist/testHost.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
/**
|
|
56
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
/**
|
|
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
|
|
176
|
-
* on every other at once, rather than on
|
|
177
|
-
* Without it
|
|
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
|
-
}):
|
|
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 {};
|