@rsc-kit/core 0.25.0 → 0.27.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.
Files changed (47) hide show
  1. package/dist/changed.d.ts +260 -0
  2. package/dist/changed.js +605 -0
  3. package/dist/changed.js.map +1 -0
  4. package/dist/conformance.d.ts +2 -0
  5. package/dist/conformance.js +28 -0
  6. package/dist/conformance.js.map +1 -1
  7. package/dist/headers.d.ts +8 -0
  8. package/dist/headers.js +8 -0
  9. package/dist/headers.js.map +1 -1
  10. package/dist/host.d.ts +3 -1
  11. package/dist/host.js +21 -1
  12. package/dist/host.js.map +1 -1
  13. package/dist/js/Changes.d.ts +11 -0
  14. package/dist/js/Changes.js +19 -0
  15. package/dist/js/Changes.js.map +1 -0
  16. package/dist/js/changesStore.d.ts +9 -0
  17. package/dist/js/changesStore.js +178 -0
  18. package/dist/js/changesStore.js.map +1 -0
  19. package/dist/js/navigate.d.ts +1 -1
  20. package/dist/js/navigate.js +4 -1
  21. package/dist/js/navigate.js.map +1 -1
  22. package/dist/js/refreshOn.d.ts +21 -0
  23. package/dist/js/refreshOn.js +58 -0
  24. package/dist/js/refreshOn.js.map +1 -0
  25. package/dist/js/router.d.ts +2 -0
  26. package/dist/js/router.js +7 -0
  27. package/dist/js/router.js.map +1 -1
  28. package/dist/js/section.d.ts +21 -1
  29. package/dist/js/section.js +31 -6
  30. package/dist/js/section.js.map +1 -1
  31. package/dist/manifest.d.ts +6 -0
  32. package/dist/manifest.js.map +1 -1
  33. package/dist/request.d.ts +11 -0
  34. package/dist/request.js +17 -0
  35. package/dist/request.js.map +1 -1
  36. package/dist/sharedRenders.d.ts +9 -0
  37. package/dist/sharedRenders.js +41 -0
  38. package/dist/sharedRenders.js.map +1 -0
  39. package/dist/testHost.d.ts +22 -0
  40. package/dist/testHost.js +23 -0
  41. package/dist/testHost.js.map +1 -1
  42. package/dist/testing.d.ts +1 -1
  43. package/dist/testing.js +7 -1
  44. package/dist/testing.js.map +1 -1
  45. package/dist/vite.js +79 -11
  46. package/dist/vite.js.map +1 -1
  47. package/package.json +5 -1
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Names: a name for data a page depends on, so whatever changes it can say so
3
+ * from anywhere - a webhook, a job, another visitor's action - and every open
4
+ * tab showing it refreshes.
5
+ *
6
+ * // a section says what it depends on
7
+ * export default section('repos', Repos, { refreshOn: ({ params }) => [`team:${params.team}:repos`] })
8
+ *
9
+ * // and whatever changes it says so
10
+ * changed(`team:${team}:repos`) // here, from a route.ts or an action
11
+ * rsckit.Changed(ctx, "team:1:repos") // or the backend, from its webhook
12
+ * Rsc::changed("team:$team:repos"); // Laravel
13
+ *
14
+ * A name has a version: a number that moves whenever something says the name
15
+ * changed. Nothing else - not what changed, not the data. A page learns the
16
+ * versions at render; one stream per open tab watches them; when one moves,
17
+ * the sections carrying that name ask for themselves again, the way refresh()
18
+ * does. Data is never pushed, which is what keeps this working on a host that
19
+ * cannot hold state: a version is a read.
20
+ *
21
+ * Where versions live is the source's business. Without a backend they are in
22
+ * this process, which is one instance. With one, the backend keeps them - in
23
+ * its cache or a table, shared by every instance it runs - and answers
24
+ * `__rsc.changed`: given the versions a watcher holds, the ones that differ now.
25
+ * A backend that can wait holds the call until one does; one that cannot
26
+ * answers at once, and the watcher asks again on its interval. The protocol
27
+ * is the same either way, and so is the page.
28
+ */
29
+ /** The reserved name a backend answers name versions on. */
30
+ export declare const CHANGED_FUNCTION = "__rsc.changed";
31
+ /** What `__rsc.changed` is asked. */
32
+ export interface ChangedQuery {
33
+ /** The versions the asker holds. A name never changed is 0; -1 asks for every current version. */
34
+ since: Record<string, number>;
35
+ /** How long the backend may hold the call waiting for one to differ, in ms. It may answer at once. */
36
+ wait?: number;
37
+ }
38
+ /** What `__rsc.changed` answers: the versions among `since` that differ now. */
39
+ export interface ChangedAnswer {
40
+ versions: Record<string, number>;
41
+ }
42
+ /**
43
+ * Where name versions are read from, and - when they live here - bumped.
44
+ *
45
+ * `changed` answers the versions among `since` that differ now, and may wait
46
+ * up to `wait` ms for one to. Nothing else is required of it, which is why a
47
+ * backend's cache, a table, this process's memory, or a pub/sub that answers
48
+ * the moment a name moves all fit behind it.
49
+ */
50
+ export interface VersionSource {
51
+ changed(since: Record<string, number>, wait: number): Promise<Record<string, number>>;
52
+ /** Say these names changed. Only a source that holds its own versions can. */
53
+ bump(names: string[]): void | Promise<void>;
54
+ }
55
+ /** Versions kept in this process: the source without a backend, and the one tests use. */
56
+ export declare function memoryVersions(): VersionSource;
57
+ type HostFn = (name: string, ...args: unknown[]) => Promise<unknown>;
58
+ /**
59
+ * Versions kept by the backend, read through `__rsc.changed`.
60
+ *
61
+ * A backend that has not got the function - an adapter from before names, or
62
+ * a host of plain rpc functions - is answered from this process instead,
63
+ * found out on the first ask and remembered, so a page with names still works
64
+ * on it: one instance, versions bumped by `changed()` here.
65
+ */
66
+ export declare function backendVersions(call: HostFn): VersionSource;
67
+ /** Runs one statement with positional parameters and resolves to its rows. */
68
+ export type SqlQuery = (text: string, params: unknown[]) => Promise<readonly Record<string, unknown>[]>;
69
+ export interface SqlVersionsOptions {
70
+ /**
71
+ * Your driver, as a function of SQL text and parameters:
72
+ *
73
+ * query: (text, params) => sql.unsafe(text, params) // postgres.js, Bun.sql
74
+ * query: async (text, params) => (await pool.query(text, params)).rows // node-postgres
75
+ */
76
+ query: SqlQuery;
77
+ /** The table: `CREATE TABLE rsc_versions (name TEXT PRIMARY KEY, version BIGINT NOT NULL)`. */
78
+ table?: string;
79
+ /** How the n-th parameter is written (1-based). Default `?`; Postgres is `(n) => '$' + n`. */
80
+ placeholder?: (n: number) => string;
81
+ /**
82
+ * Called once with a function that wakes every waiting ask, for a database
83
+ * that can say the moment a version moved - Postgres's LISTEN:
84
+ *
85
+ * listen: (wake) => sql.listen('rsc_versions', wake)
86
+ *
87
+ * Without it, a waiting ask reads the table every `poll` ms.
88
+ */
89
+ listen?: (wake: () => void) => unknown;
90
+ /**
91
+ * Run after every bump, to tell the other instances - Postgres's NOTIFY:
92
+ *
93
+ * notify: () => sql.notify('rsc_versions', '')
94
+ */
95
+ notify?: () => unknown;
96
+ /** How often a waiting ask reads the table. Default 1000ms. */
97
+ poll?: number;
98
+ }
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;
116
+ /**
117
+ * Where versions are kept, as the app writes it for its own database: the
118
+ * three things rsc-kit cannot know about your storage, and nothing else.
119
+ * `createVersions` does the rest - comparing, waiting, waking.
120
+ *
121
+ * // Drizzle
122
+ * read: async (names) => Object.fromEntries(
123
+ * (await db.select().from(rscVersions).where(inArray(rscVersions.name, names)))
124
+ * .map((r) => [r.name, Number(r.version)])),
125
+ * bump: async (names) => { for (const name of names) await db.insert(rscVersions)
126
+ * .values({ name, version: 1 })
127
+ * .onConflictDoUpdate({ target: rscVersions.name, set: { version: sql`${rscVersions.version} + 1` } }) },
128
+ *
129
+ * // Redis
130
+ * read: async (names) => Object.fromEntries((await redis.mget(names.map(k)))
131
+ * .map((v, i) => [names[i], Number(v ?? 0)])),
132
+ * bump: async (names) => { await Promise.all(names.map((n) => redis.incr(k(n)))) },
133
+ */
134
+ export interface VersionStore {
135
+ /** The current version of each name asked for. A name it has never seen may be left out: it is 0. */
136
+ 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. */
138
+ bump(names: string[]): Promise<void>;
139
+ /**
140
+ * Called once with a function that wakes every waiting ask, when the store
141
+ * can say the moment a version moved - Postgres LISTEN, Redis SUBSCRIBE.
142
+ * Without it a waiting ask reads again every `poll` ms.
143
+ */
144
+ listen?(wake: () => void): unknown;
145
+ /** Run after a bump, to tell the other instances listening: Postgres NOTIFY, Redis PUBLISH. */
146
+ notify?(): unknown;
147
+ }
148
+ /**
149
+ * A version source over any store: SQL through any driver or ORM - Drizzle,
150
+ * Prisma, Kysely, Knex - or Redis, or anything with a read and an increment.
151
+ *
152
+ * installVersionSource(createVersions({ read, bump, listen, notify }))
153
+ *
154
+ * rsc-kit owns the rest: an ask waits up to the time it was given for a
155
+ * version to differ, reading every `poll` ms (1000 by default) or the moment
156
+ * `listen` wakes it, and a bump in this process wakes this process's asks at
157
+ * once whatever the store.
158
+ */
159
+ export declare function createVersions(store: VersionStore, options?: {
160
+ poll?: number;
161
+ }): VersionSource;
162
+ /** The parts of a Postgres client `postgresVersions` uses: postgres.js and `Bun.sql` both have them. */
163
+ export interface PostgresClient {
164
+ unsafe(text: string, params?: unknown[]): PromiseLike<readonly Record<string, unknown>[]>;
165
+ /** postgres.js has it; with it, a waiting ask hears another instance at once. */
166
+ listen?(channel: string, onNotify: () => void): unknown;
167
+ notify?(channel: string, payload: string): unknown;
168
+ }
169
+ /**
170
+ * Versions in Postgres, from the client the app already has:
171
+ *
172
+ * installVersionSource(postgresVersions(sql))
173
+ *
174
+ * `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.
178
+ *
179
+ * CREATE TABLE rsc_versions (name TEXT PRIMARY KEY, version BIGINT NOT NULL)
180
+ */
181
+ export declare function postgresVersions(sql: PostgresClient, options?: {
182
+ table?: string;
183
+ channel?: string;
184
+ poll?: number;
185
+ }): VersionSource;
186
+ /**
187
+ * Install where versions are read from - a table, Redis, any store - for this
188
+ * process; `null` takes it out again.
189
+ *
190
+ * The app's choice wins over the backend's: with a Go or Laravel backend that
191
+ * writes its versions to a store this process can read too, reading it here
192
+ * means watching costs the backend nothing.
193
+ */
194
+ export declare function installVersionSource(source: VersionSource | null): void;
195
+ /** The engine's, when a backend is installed: its versions, read through it. Not for apps. */
196
+ export declare function installBackendVersionSource(source: VersionSource | null): void;
197
+ /** The source in use: the app's, else the backend's, else this process's own. */
198
+ export declare function versionSource(): VersionSource;
199
+ /**
200
+ * Say these names changed, so every open tab showing them refreshes.
201
+ *
202
+ * export async function POST(request: Request) { // a webhook, in a route.ts
203
+ * const { team } = await request.json()
204
+ * changed(`team:${team}:repos`)
205
+ * return new Response(null, { status: 204 })
206
+ * }
207
+ *
208
+ * For an app that keeps its versions here. With a backend the versions are
209
+ * its, and so is the call: `rsckit.Changed`, `Rsc::changed`.
210
+ */
211
+ export declare function changed(...names: string[]): Promise<void>;
212
+ /** Thrown when a production server has no key to sign names with. */
213
+ export declare class MissingSigningSecret extends Error {
214
+ constructor();
215
+ }
216
+ /**
217
+ * The secret names are signed with, when it does not come from the
218
+ * environment. Normally it does: `RSC_SIGNING_SECRET`, the same on every
219
+ * instance, or a tab served by one is refused by another.
220
+ *
221
+ * Its own secret, not the backend's host-call secret: that one authorises
222
+ * calls between the renderer and a backend, and an app with no backend has
223
+ * no reason to have it. Two jobs, two keys - one leaking does not hand out
224
+ * the other.
225
+ */
226
+ export declare function configureChanged(options: {
227
+ secret?: string | null;
228
+ }): void;
229
+ /**
230
+ * Refuse, in production, to sign with a key nobody else has.
231
+ *
232
+ * A random key is right for one process - a dev server - and silently wrong
233
+ * for two: every tab served by one instance is refused by the next, and never
234
+ * refreshes. The server calls this on its first request when the app uses
235
+ * refreshOn, so the misconfiguration is an error at once rather than a page
236
+ * that quietly stops updating.
237
+ */
238
+ export declare function assertSigningSecret(): void;
239
+ /** A name's signature: the first 16 bytes of an HMAC, base64url, which is what rides in a url. */
240
+ export declare function sign(name: string): Promise<string>;
241
+ /** What a page hands the client for one name: its version at render, and its signature. */
242
+ export type SignedName = [version: number, signature: string];
243
+ /** How often the source is asked, when it answered at once. */
244
+ export declare const CHANGES_INTERVAL_MS = 2000;
245
+ /** How long the source may hold one ask. Bounds how long a new tab waits to join the shared ask. */
246
+ export declare const CHANGES_WAIT_MS = 5000;
247
+ /**
248
+ * The stream a tab watches its names on: `GET /_rsc/changes?w=[[name, version, signature], …]`.
249
+ *
250
+ * One `data:` line per version that moved, `{ name, version }`, from the moment
251
+ * the stream opens - a name that moved between the render and now is reported
252
+ * at once, so nothing that happened in the gap is missed. Keepalives as an
253
+ * events() route sends them; ends when the browser goes away.
254
+ */
255
+ export declare function changes(request: Request): Promise<Response>;
256
+ /** For tests: forget every stream and version this process has seen. */
257
+ export declare function resetChanges(): void;
258
+ /** The url a tab opens to watch, for the client. */
259
+ export declare const CHANGES_PATH: "/_rsc/changes";
260
+ export {};