@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
package/dist/changed.js
CHANGED
|
@@ -26,14 +26,52 @@
|
|
|
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
|
-
import {
|
|
29
|
+
import { frame, keepaliveMs } from "./events.js";
|
|
30
30
|
import { HEADER } from "./headers.js";
|
|
31
|
+
import { listenToBroadcast } from "./broadcast.js";
|
|
32
|
+
export { listenToBroadcast } from "./broadcast.js";
|
|
31
33
|
/** The reserved name a backend answers name versions on. */
|
|
32
34
|
export const CHANGED_FUNCTION = "__rsc.changed";
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The version a name moves to: the larger of one past where it was and the
|
|
37
|
+
* current time in milliseconds.
|
|
38
|
+
*
|
|
39
|
+
* A counter would do for "it moved", but a counter repeats once its row is
|
|
40
|
+
* gone - deleted to keep the store small, a name starts again from 0 and
|
|
41
|
+
* climbs back to a value some tab is still holding, and that tab misses the
|
|
42
|
+
* change. A time never comes round again, so a name may be forgotten at any
|
|
43
|
+
* moment: a tab holding the old value sees a different one, and at worst
|
|
44
|
+
* refreshes once for nothing. It is also when the name last moved, which is
|
|
45
|
+
* all cleanup needs. Every store bumps with it; one that writes `+ 1` still
|
|
46
|
+
* works, it is just not safe to prune.
|
|
47
|
+
*/
|
|
48
|
+
export function nextVersion(current = 0) {
|
|
49
|
+
return Math.max(current + 1, Date.now());
|
|
50
|
+
}
|
|
51
|
+
/** The safety-net read of a store that listens: longer than any ask waits, so once per ask. */
|
|
52
|
+
export const LISTENING_POLL_MS = 30_000;
|
|
53
|
+
/** How long a name nobody changes is kept, by the stores that forget on their own. */
|
|
54
|
+
export const VERSIONS_KEPT_MS = 30 * 24 * 60 * 60 * 1000;
|
|
55
|
+
/**
|
|
56
|
+
* Versions kept in this process: the source without a backend, and the one
|
|
57
|
+
* tests use. A name not changed in `forgetAfter` ms (30 days) is forgotten,
|
|
58
|
+
* swept at most once an hour, so a process up for months does not hold every
|
|
59
|
+
* name it ever saw.
|
|
60
|
+
*/
|
|
61
|
+
export function memoryVersions(options = {}) {
|
|
62
|
+
const forgetAfter = options.forgetAfter ?? VERSIONS_KEPT_MS;
|
|
35
63
|
const versions = new Map();
|
|
36
64
|
const waiters = new Set();
|
|
65
|
+
let swept = Date.now();
|
|
66
|
+
const sweep = () => {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (now - swept < Math.min(forgetAfter, 60 * 60 * 1000))
|
|
69
|
+
return;
|
|
70
|
+
swept = now;
|
|
71
|
+
for (const [name, version] of versions)
|
|
72
|
+
if (version < now - forgetAfter)
|
|
73
|
+
versions.delete(name);
|
|
74
|
+
};
|
|
37
75
|
const differing = (since) => {
|
|
38
76
|
const out = {};
|
|
39
77
|
for (const [name, held] of Object.entries(since)) {
|
|
@@ -62,8 +100,9 @@ export function memoryVersions() {
|
|
|
62
100
|
return differing(since);
|
|
63
101
|
},
|
|
64
102
|
bump(names) {
|
|
103
|
+
sweep();
|
|
65
104
|
for (const name of names)
|
|
66
|
-
versions.set(name, (versions.get(name)
|
|
105
|
+
versions.set(name, nextVersion(versions.get(name)));
|
|
67
106
|
for (const wake of [...waiters])
|
|
68
107
|
wake();
|
|
69
108
|
},
|
|
@@ -123,39 +162,88 @@ export function backendVersions(call) {
|
|
|
123
162
|
* SQL dialect has - rather than an upsert each spells differently. Takes the
|
|
124
163
|
* app's own driver, so the engine has no database dependency of its own.
|
|
125
164
|
*/
|
|
165
|
+
/**
|
|
166
|
+
* One statement that moves every name to nextVersion: an upsert, in the
|
|
167
|
+
* dialect's own words, and on Postgres the NOTIFY with it. A name is either
|
|
168
|
+
* inserted at `now` or moved to the larger of one more and `now`.
|
|
169
|
+
*/
|
|
170
|
+
function upsert(dialect, table, mark, names, now, channel) {
|
|
171
|
+
if (dialect === "postgres") {
|
|
172
|
+
// $1 is now, the names follow; the channel, if any, comes last.
|
|
173
|
+
const rows = names.map((_, i) => `(${mark(i + 2)}, ${mark(1)})`).join(", ");
|
|
174
|
+
const insert = `INSERT INTO ${table} (name, version) VALUES ${rows} ` +
|
|
175
|
+
`ON CONFLICT (name) DO UPDATE SET version = GREATEST(${table}.version + 1, EXCLUDED.version)`;
|
|
176
|
+
if (!channel)
|
|
177
|
+
return [insert, [now, ...names]];
|
|
178
|
+
// A NOTIFY is delivered when the transaction commits, so a listener is
|
|
179
|
+
// never woken before the change it is told about can be read.
|
|
180
|
+
return [
|
|
181
|
+
`WITH moved AS (${insert} RETURNING 1) SELECT pg_notify(${mark(names.length + 2)}, '') FROM (SELECT count(*) FROM moved) AS done`,
|
|
182
|
+
[now, ...names, channel],
|
|
183
|
+
];
|
|
184
|
+
}
|
|
185
|
+
const rows = names.map(() => `(${mark(1)}, ${mark(1)})`).join(", ");
|
|
186
|
+
const params = names.flatMap((name) => [name, now]);
|
|
187
|
+
return dialect === "mysql"
|
|
188
|
+
? [
|
|
189
|
+
`INSERT INTO ${table} (name, version) VALUES ${rows} ON DUPLICATE KEY UPDATE version = GREATEST(version + 1, VALUES(version))`,
|
|
190
|
+
params,
|
|
191
|
+
]
|
|
192
|
+
: [
|
|
193
|
+
`INSERT INTO ${table} (name, version) VALUES ${rows} ON CONFLICT (name) DO UPDATE SET version = MAX(version + 1, excluded.version)`,
|
|
194
|
+
params,
|
|
195
|
+
];
|
|
196
|
+
}
|
|
126
197
|
export function sqlVersions(options) {
|
|
127
198
|
const { query } = options;
|
|
128
199
|
const table = options.table ?? "rsc_versions";
|
|
129
200
|
const mark = options.placeholder ?? (() => "?");
|
|
130
|
-
|
|
201
|
+
const source = createVersions({
|
|
131
202
|
async read(names) {
|
|
132
203
|
const rows = await query(`SELECT name, version FROM ${table} WHERE name IN (${names.map((_, i) => mark(i + 1)).join(", ")})`, names);
|
|
133
204
|
// A BIGINT comes back as a string from most drivers.
|
|
134
205
|
return Object.fromEntries(rows.map((row) => [String(row.name), Number(row.version)]));
|
|
135
206
|
},
|
|
136
207
|
async bump(names) {
|
|
137
|
-
|
|
138
|
-
|
|
208
|
+
if (options.dialect) {
|
|
209
|
+
await query(...upsert(options.dialect, table, mark, names, Date.now(), options.channel));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// nextVersion, in SQL every dialect has: CASE rather than GREATEST,
|
|
213
|
+
// which SQLite spells MAX. The time is this process's, passed in, so
|
|
214
|
+
// the database's clock never has to agree with it.
|
|
215
|
+
const update = `UPDATE ${table} SET version = CASE WHEN version + 1 > ${mark(1)} THEN version + 1 ELSE ${mark(2)} END ` +
|
|
216
|
+
`WHERE name = ${mark(3)}`;
|
|
217
|
+
const insert = `INSERT INTO ${table} (name, version) VALUES (${mark(1)}, ${mark(2)})`;
|
|
139
218
|
for (const name of names) {
|
|
219
|
+
const now = Date.now();
|
|
140
220
|
// The row count is not something every driver reports the same way,
|
|
141
221
|
// so ask: a name with no row yet gets one.
|
|
142
222
|
const [exists] = await query(`SELECT 1 AS found FROM ${table} WHERE name = ${mark(1)}`, [name]);
|
|
143
223
|
if (exists) {
|
|
144
|
-
await query(update, [name]);
|
|
224
|
+
await query(update, [now, now, name]);
|
|
145
225
|
continue;
|
|
146
226
|
}
|
|
147
227
|
try {
|
|
148
|
-
await query(insert, [name]);
|
|
228
|
+
await query(insert, [name, now]);
|
|
149
229
|
}
|
|
150
230
|
catch {
|
|
151
231
|
// Another instance inserted it first: bump that one.
|
|
152
|
-
await query(update, [name]);
|
|
232
|
+
await query(update, [now, now, name]);
|
|
153
233
|
}
|
|
154
234
|
}
|
|
155
235
|
},
|
|
156
236
|
listen: options.listen,
|
|
157
237
|
notify: options.notify,
|
|
158
238
|
}, { poll: options.poll });
|
|
239
|
+
return {
|
|
240
|
+
...source,
|
|
241
|
+
async prune(olderThan = VERSIONS_KEPT_MS) {
|
|
242
|
+
await query(`DELETE FROM ${table} WHERE version < ${mark(1)}`, [
|
|
243
|
+
Date.now() - olderThan,
|
|
244
|
+
]);
|
|
245
|
+
},
|
|
246
|
+
};
|
|
159
247
|
}
|
|
160
248
|
/**
|
|
161
249
|
* A version source over any store: SQL through any driver or ORM - Drizzle,
|
|
@@ -169,14 +257,22 @@ export function sqlVersions(options) {
|
|
|
169
257
|
* once whatever the store.
|
|
170
258
|
*/
|
|
171
259
|
export function createVersions(store, options = {}) {
|
|
172
|
-
|
|
260
|
+
// A store that listens is woken the moment a version moves, so reading the
|
|
261
|
+
// table on a timer is only a safety net - for a listening connection that
|
|
262
|
+
// dropped without a word. Once an ask is enough: about every five seconds
|
|
263
|
+
// per server, where one that cannot listen reads every second.
|
|
264
|
+
const poll = options.poll ?? (store.listen ? LISTENING_POLL_MS : 1_000);
|
|
173
265
|
const waiters = new Set();
|
|
174
266
|
const wakeAll = () => {
|
|
175
267
|
for (const wake of [...waiters])
|
|
176
268
|
wake();
|
|
177
269
|
};
|
|
270
|
+
// A store that can listen wakes the renderer the way anything else does -
|
|
271
|
+
// through wakeOn, which starts it with the first watching tab and never in
|
|
272
|
+
// a build. A wake also ends an ask this store is holding.
|
|
178
273
|
if (store.listen)
|
|
179
|
-
|
|
274
|
+
wakeOn(store.listen);
|
|
275
|
+
wakers().sleepers.add(wakeAll);
|
|
180
276
|
const read = async (names) => {
|
|
181
277
|
if (names.length === 0)
|
|
182
278
|
return {};
|
|
@@ -185,6 +281,10 @@ export function createVersions(store, options = {}) {
|
|
|
185
281
|
};
|
|
186
282
|
return {
|
|
187
283
|
async changed(since, wait) {
|
|
284
|
+
// Only a server with a tab watching asks with a wait - never a build -
|
|
285
|
+
// so this is a safe moment to start listening, as is the first tab.
|
|
286
|
+
if (wait > 0)
|
|
287
|
+
startWakers();
|
|
188
288
|
const names = Object.keys(since);
|
|
189
289
|
const deadline = Date.now() + Math.max(0, wait);
|
|
190
290
|
for (;;) {
|
|
@@ -225,23 +325,26 @@ export function createVersions(store, options = {}) {
|
|
|
225
325
|
* installVersionSource(postgresVersions(sql))
|
|
226
326
|
*
|
|
227
327
|
* `sqlVersions` with Postgres's placeholders, and - when the client can
|
|
228
|
-
* LISTEN, as postgres.js can - a change on one
|
|
229
|
-
* on every other at once, rather than on
|
|
230
|
-
* Without it
|
|
328
|
+
* LISTEN, as postgres.js and `Bun.sql` (Bun 1.4+) can - a change on one
|
|
329
|
+
* instance wakes the waiting asks on every other at once, rather than on
|
|
330
|
+
* their next read of the table. Without it, the table is read once a second
|
|
331
|
+
* while an ask waits.
|
|
231
332
|
*
|
|
232
333
|
* CREATE TABLE rsc_versions (name TEXT PRIMARY KEY, version BIGINT NOT NULL)
|
|
233
334
|
*/
|
|
234
335
|
export function postgresVersions(sql, options = {}) {
|
|
235
336
|
const channel = options.channel ?? "rsc_versions";
|
|
236
337
|
const listen = sql.listen?.bind(sql);
|
|
237
|
-
|
|
338
|
+
// One statement per change: the upsert of every name, and the NOTIFY that
|
|
339
|
+
// wakes every listening server - this one included - when it commits.
|
|
238
340
|
return sqlVersions({
|
|
239
341
|
query: async (text, params) => await sql.unsafe(text, params),
|
|
240
342
|
placeholder: (n) => "$" + n,
|
|
343
|
+
dialect: "postgres",
|
|
344
|
+
channel,
|
|
241
345
|
table: options.table,
|
|
242
346
|
poll: options.poll,
|
|
243
347
|
listen: listen ? (wake) => listen(channel, wake) : undefined,
|
|
244
|
-
notify: listen && notify ? () => notify(channel, "") : undefined,
|
|
245
348
|
});
|
|
246
349
|
}
|
|
247
350
|
const SOURCE = Symbol.for("rsc-kit.version-source");
|
|
@@ -265,9 +368,28 @@ export function installBackendVersionSource(source) {
|
|
|
265
368
|
}
|
|
266
369
|
/** The source in use: the app's, else the backend's, else this process's own. */
|
|
267
370
|
export function versionSource() {
|
|
268
|
-
|
|
269
|
-
globals[BACKEND_SOURCE]
|
|
270
|
-
|
|
371
|
+
const chosen = globals[SOURCE] ??
|
|
372
|
+
globals[BACKEND_SOURCE];
|
|
373
|
+
if (chosen)
|
|
374
|
+
return chosen;
|
|
375
|
+
let local = globals[LOCAL_SOURCE];
|
|
376
|
+
if (!local) {
|
|
377
|
+
local = globals[LOCAL_SOURCE] = memoryVersions();
|
|
378
|
+
// The fallback works in development and in tests, which are one process,
|
|
379
|
+
// and fails silently in production with two: a change on one instance
|
|
380
|
+
// never reaches the tabs on another. Said once, at the first use. An app
|
|
381
|
+
// that really is one instance installs memoryVersions() itself, which is
|
|
382
|
+
// a choice rather than a fallback, and is not warned.
|
|
383
|
+
if (production()) {
|
|
384
|
+
console.warn("[rsc-kit] refreshOn versions are kept in this process only: a change made on another " +
|
|
385
|
+
"instance, or in a worker, will not reach the tabs this one serves. Install a store every " +
|
|
386
|
+
"process shares with installVersionSource() in instrumentation.ts - sqlVersions for MySQL " +
|
|
387
|
+
"or SQLite, postgresVersions for Postgres, createVersions for anything else: " +
|
|
388
|
+
"https://docs.rsc-kit.dev/guides/sections#where-the-versions-live. For a deploy that is one " +
|
|
389
|
+
"instance, installVersionSource(memoryVersions()) says so.");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return local;
|
|
271
393
|
}
|
|
272
394
|
/**
|
|
273
395
|
* Say these names changed, so every open tab showing them refreshes.
|
|
@@ -427,6 +549,26 @@ function observe(name, version) {
|
|
|
427
549
|
if (stream.names.has(name))
|
|
428
550
|
stream.send(name, version);
|
|
429
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* A stream gone: and with it every name no other stream holds, which this
|
|
554
|
+
* process has no reason to remember. Kept, the latest version of every name
|
|
555
|
+
* any tab ever watched piled up for as long as the server ran.
|
|
556
|
+
*/
|
|
557
|
+
function close(stream) {
|
|
558
|
+
if (!streams.delete(stream))
|
|
559
|
+
return;
|
|
560
|
+
for (const name of stream.names) {
|
|
561
|
+
let held = false;
|
|
562
|
+
for (const other of streams) {
|
|
563
|
+
if (other.names.has(name)) {
|
|
564
|
+
held = true;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!held)
|
|
569
|
+
known.delete(name);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
430
572
|
/** Mapped to this process's set of streams: what to ask about, relative to what is known. */
|
|
431
573
|
function sinceAll() {
|
|
432
574
|
const since = {};
|
|
@@ -435,11 +577,85 @@ function sinceAll() {
|
|
|
435
577
|
since[name] = known.get(name) ?? 0;
|
|
436
578
|
return since;
|
|
437
579
|
}
|
|
580
|
+
// ── Waking ───────────────────────────────────────────────────────────────────
|
|
581
|
+
//
|
|
582
|
+
// A backend that cannot hold a question open - PHP - is asked again on an
|
|
583
|
+
// interval. Something that can say "a version moved" the moment it does -
|
|
584
|
+
// a broadcast, a Redis channel - makes that interval a safety net: the
|
|
585
|
+
// renderer asks the moment it is woken, and otherwise rarely.
|
|
586
|
+
const WAKERS = Symbol.for("rsc-kit.wakers");
|
|
587
|
+
function wakers() {
|
|
588
|
+
return (globals[WAKERS] ??= {
|
|
589
|
+
listens: [],
|
|
590
|
+
started: false,
|
|
591
|
+
pending: false,
|
|
592
|
+
sleepers: new Set(),
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Wake the renderer when a version may have moved, so it asks then rather
|
|
597
|
+
* than on its next interval.
|
|
598
|
+
*
|
|
599
|
+
* wakeOn(listenToBroadcast({ url, key })) // a broadcast server
|
|
600
|
+
* wakeOn((wake) => subscriber.subscribe('rsc', wake)) // a Redis channel
|
|
601
|
+
*
|
|
602
|
+
* `listen` is given a function to call - it carries no names and no data;
|
|
603
|
+
* the renderer asks wherever the versions live, as always. It is started
|
|
604
|
+
* when the first tab starts watching, never in a build. With one installed,
|
|
605
|
+
* a backend that answers at once is asked again every 30 seconds as a safety
|
|
606
|
+
* net instead of every two.
|
|
607
|
+
*/
|
|
608
|
+
export function wakeOn(listen) {
|
|
609
|
+
const state = wakers();
|
|
610
|
+
state.listens.push(listen);
|
|
611
|
+
if (state.started)
|
|
612
|
+
start(listen);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Start one listener; one that fails - a database not up yet - is tried
|
|
616
|
+
* again, backing off to thirty seconds. Until it is up the timer stands in.
|
|
617
|
+
*/
|
|
618
|
+
function start(listen, failures = 0) {
|
|
619
|
+
void Promise.resolve()
|
|
620
|
+
.then(() => listen(signal))
|
|
621
|
+
.catch((error) => {
|
|
622
|
+
if (failures === 0) {
|
|
623
|
+
console.warn("[rsc-kit] a wakeOn listener failed to start, trying again: " +
|
|
624
|
+
(error instanceof Error ? error.message : String(error)));
|
|
625
|
+
}
|
|
626
|
+
const retry = setTimeout(() => start(listen, failures + 1), Math.min(30_000, 1000 * 2 ** failures));
|
|
627
|
+
retry.unref?.();
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/** Something said a version may have moved: ask now, and end any ask a store is holding. */
|
|
631
|
+
function signal() {
|
|
632
|
+
const state = wakers();
|
|
633
|
+
state.pending = true;
|
|
634
|
+
wake?.();
|
|
635
|
+
for (const sleeper of [...state.sleepers])
|
|
636
|
+
sleeper();
|
|
637
|
+
}
|
|
638
|
+
function startWakers() {
|
|
639
|
+
const state = wakers();
|
|
640
|
+
if (state.started)
|
|
641
|
+
return;
|
|
642
|
+
state.started = true;
|
|
643
|
+
// Set in the environment, not guessed: a broadcast server to listen to.
|
|
644
|
+
const url = env("RSC_BROADCAST_URL");
|
|
645
|
+
const key = env("RSC_BROADCAST_KEY");
|
|
646
|
+
if (url && key)
|
|
647
|
+
state.listens.push(listenToBroadcast({ url, key }));
|
|
648
|
+
for (const listen of state.listens)
|
|
649
|
+
start(listen);
|
|
650
|
+
}
|
|
438
651
|
async function loop() {
|
|
439
652
|
running = true;
|
|
653
|
+
startWakers();
|
|
440
654
|
try {
|
|
441
655
|
while (streams.size > 0) {
|
|
442
656
|
const started = Date.now();
|
|
657
|
+
const state = wakers();
|
|
658
|
+
state.pending = false;
|
|
443
659
|
try {
|
|
444
660
|
const moved = await versionSource().changed(sinceAll(), CHANGES_WAIT_MS);
|
|
445
661
|
for (const [name, version] of Object.entries(moved))
|
|
@@ -454,8 +670,11 @@ async function loop() {
|
|
|
454
670
|
}
|
|
455
671
|
}
|
|
456
672
|
// A source that waited used the interval; one that answered at once is
|
|
457
|
-
// asked again after it
|
|
458
|
-
|
|
673
|
+
// asked again after it - or, with something to wake it, only as a
|
|
674
|
+
// safety net. A stream opening, or a wake, ends the wait early; a wake
|
|
675
|
+
// that came during the ask is answered at once.
|
|
676
|
+
const interval = state.listens.length > 0 ? LISTENING_POLL_MS : CHANGES_INTERVAL_MS;
|
|
677
|
+
const remaining = state.pending ? 0 : interval - (Date.now() - started);
|
|
459
678
|
if (remaining > 0 && streams.size > 0) {
|
|
460
679
|
await new Promise((resolve) => {
|
|
461
680
|
const timer = setTimeout(resolve, remaining);
|
|
@@ -537,7 +756,7 @@ export async function changes(request) {
|
|
|
537
756
|
}
|
|
538
757
|
};
|
|
539
758
|
write("retry: 2000\n\n");
|
|
540
|
-
const keepalive = setInterval(() => write(": keepalive\n\n"),
|
|
759
|
+
const keepalive = setInterval(() => write(": keepalive\n\n"), keepaliveMs());
|
|
541
760
|
keepalive.unref?.();
|
|
542
761
|
const me = {
|
|
543
762
|
names: new Set(Object.keys(since)),
|
|
@@ -547,7 +766,7 @@ export async function changes(request) {
|
|
|
547
766
|
streams.add(me);
|
|
548
767
|
const end = () => {
|
|
549
768
|
clearInterval(keepalive);
|
|
550
|
-
|
|
769
|
+
close(me);
|
|
551
770
|
try {
|
|
552
771
|
sink.close();
|
|
553
772
|
}
|
|
@@ -582,19 +801,26 @@ export async function changes(request) {
|
|
|
582
801
|
},
|
|
583
802
|
cancel() {
|
|
584
803
|
if (stream)
|
|
585
|
-
|
|
804
|
+
close(stream);
|
|
586
805
|
},
|
|
587
806
|
});
|
|
588
807
|
return new Response(body, {
|
|
589
808
|
headers: {
|
|
590
809
|
"Content-Type": "text/event-stream; charset=utf-8",
|
|
591
|
-
|
|
810
|
+
// no-transform: a proxy or CDN in front must not compress or buffer
|
|
811
|
+
// it either - Cloudflare compresses text unless told not to.
|
|
812
|
+
"Cache-Control": "no-store, no-transform",
|
|
592
813
|
"X-Accel-Buffering": "no",
|
|
593
814
|
},
|
|
594
815
|
});
|
|
595
816
|
}
|
|
596
817
|
/** For tests: forget every stream and version this process has seen. */
|
|
818
|
+
/** For tests: how many names this process remembers for its open streams. */
|
|
819
|
+
export function rememberedNames() {
|
|
820
|
+
return known.size;
|
|
821
|
+
}
|
|
597
822
|
export function resetChanges() {
|
|
823
|
+
delete globals[WAKERS];
|
|
598
824
|
streams.clear();
|
|
599
825
|
known.clear();
|
|
600
826
|
warned = false;
|