@rsc-kit/core 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/broadcast.d.ts +22 -0
- package/dist/broadcast.js +109 -0
- package/dist/broadcast.js.map +1 -0
- package/dist/changed.d.ts +114 -31
- package/dist/changed.js +271 -37
- package/dist/changed.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
|
@@ -28,12 +28,50 @@
|
|
|
28
28
|
*/
|
|
29
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,25 +257,30 @@ 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
|
+
let listening = Boolean(store.listen);
|
|
265
|
+
const poll = () => options.poll ?? (listening ? LISTENING_POLL_MS : 1_000);
|
|
173
266
|
const waiters = new Set();
|
|
174
267
|
const wakeAll = () => {
|
|
175
268
|
for (const wake of [...waiters])
|
|
176
269
|
wake();
|
|
177
270
|
};
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
271
|
+
// A store that can listen wakes the renderer the way anything else does -
|
|
272
|
+
// through wakeOn, which starts it with the first watching tab and never in
|
|
273
|
+
// a build. A wake also ends an ask this store is holding.
|
|
274
|
+
if (store.listen) {
|
|
275
|
+
const listen = store.listen.bind(store);
|
|
276
|
+
wakeOn((wake, health) => listen(wake, {
|
|
277
|
+
deaf: (reason) => {
|
|
278
|
+
listening = false;
|
|
279
|
+
health.deaf(reason);
|
|
280
|
+
},
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
wakers().sleepers.add(wakeAll);
|
|
191
284
|
const read = async (names) => {
|
|
192
285
|
if (names.length === 0)
|
|
193
286
|
return {};
|
|
@@ -196,8 +289,10 @@ export function createVersions(store, options = {}) {
|
|
|
196
289
|
};
|
|
197
290
|
return {
|
|
198
291
|
async changed(since, wait) {
|
|
292
|
+
// Only a server with a tab watching asks with a wait - never a build -
|
|
293
|
+
// so this is a safe moment to start listening, as is the first tab.
|
|
199
294
|
if (wait > 0)
|
|
200
|
-
|
|
295
|
+
startWakers();
|
|
201
296
|
const names = Object.keys(since);
|
|
202
297
|
const deadline = Date.now() + Math.max(0, wait);
|
|
203
298
|
for (;;) {
|
|
@@ -215,7 +310,7 @@ export function createVersions(store, options = {}) {
|
|
|
215
310
|
waiters.delete(done);
|
|
216
311
|
resolve();
|
|
217
312
|
};
|
|
218
|
-
const timer = setTimeout(done, Math.min(poll, remaining));
|
|
313
|
+
const timer = setTimeout(done, Math.min(poll(), remaining));
|
|
219
314
|
timer.unref?.();
|
|
220
315
|
waiters.add(done);
|
|
221
316
|
});
|
|
@@ -238,25 +333,58 @@ export function createVersions(store, options = {}) {
|
|
|
238
333
|
* installVersionSource(postgresVersions(sql))
|
|
239
334
|
*
|
|
240
335
|
* `sqlVersions` with Postgres's placeholders, and - when the client can
|
|
241
|
-
* LISTEN, as postgres.js can - a change on one
|
|
242
|
-
* on every other at once, rather than on
|
|
243
|
-
* Without it
|
|
336
|
+
* LISTEN, as postgres.js and `Bun.sql` (Bun 1.4+) can - a change on one
|
|
337
|
+
* instance wakes the waiting asks on every other at once, rather than on
|
|
338
|
+
* their next read of the table. Without it, the table is read once a second
|
|
339
|
+
* while an ask waits.
|
|
244
340
|
*
|
|
245
341
|
* CREATE TABLE rsc_versions (name TEXT PRIMARY KEY, version BIGINT NOT NULL)
|
|
246
342
|
*/
|
|
247
343
|
export function postgresVersions(sql, options = {}) {
|
|
248
344
|
const channel = options.channel ?? "rsc_versions";
|
|
249
|
-
const
|
|
250
|
-
const
|
|
345
|
+
const listener = options.listenWith ?? sql;
|
|
346
|
+
const listenOn = options.listen === false ? undefined : listener.listen?.bind(listener);
|
|
347
|
+
// One statement per change: the upsert of every name, and the NOTIFY that
|
|
348
|
+
// wakes every listening server - this one included - when it commits.
|
|
251
349
|
return sqlVersions({
|
|
252
350
|
query: async (text, params) => await sql.unsafe(text, params),
|
|
253
351
|
placeholder: (n) => "$" + n,
|
|
352
|
+
dialect: "postgres",
|
|
353
|
+
channel,
|
|
254
354
|
table: options.table,
|
|
255
355
|
poll: options.poll,
|
|
256
|
-
listen:
|
|
257
|
-
|
|
356
|
+
listen: listenOn
|
|
357
|
+
? async (wake, health) => {
|
|
358
|
+
// A probe, sent the way a change is - through `sql` - and listened
|
|
359
|
+
// for on the listening connection. Behind a transaction-mode pooler
|
|
360
|
+
// the LISTEN lands on a connection the pooler takes back, sending
|
|
361
|
+
// still works, and nothing says the listener is deaf: changes from
|
|
362
|
+
// other instances would wait for the safety-net read. The probe
|
|
363
|
+
// not arriving is how that is found out - once, as listening starts.
|
|
364
|
+
const probe = "rsc-kit:probe:" + crypto.randomUUID();
|
|
365
|
+
let heard = false;
|
|
366
|
+
await listenOn(channel, (payload) => {
|
|
367
|
+
if (payload === probe)
|
|
368
|
+
heard = true;
|
|
369
|
+
else
|
|
370
|
+
wake();
|
|
371
|
+
});
|
|
372
|
+
await sql.unsafe("SELECT pg_notify($1, $2)", [channel, probe]);
|
|
373
|
+
const timer = setTimeout(() => {
|
|
374
|
+
if (heard)
|
|
375
|
+
return;
|
|
376
|
+
health.deaf("listening on Postgres receives no notifications - is the connection going through a pooler " +
|
|
377
|
+
"in transaction mode (PgBouncer; DigitalOcean, Supabase or Neon pooled connections)? " +
|
|
378
|
+
"Pass a direct or session-mode connection: postgresVersions(sql, { listenWith }). " +
|
|
379
|
+
"Reading every second meanwhile.");
|
|
380
|
+
}, LISTEN_PROBE_MS);
|
|
381
|
+
timer.unref?.();
|
|
382
|
+
}
|
|
383
|
+
: undefined,
|
|
258
384
|
});
|
|
259
385
|
}
|
|
386
|
+
/** How long a listener's probe may take to come back before it counts as deaf. */
|
|
387
|
+
export const LISTEN_PROBE_MS = 5_000;
|
|
260
388
|
const SOURCE = Symbol.for("rsc-kit.version-source");
|
|
261
389
|
const BACKEND_SOURCE = Symbol.for("rsc-kit.backend-version-source");
|
|
262
390
|
const LOCAL_SOURCE = Symbol.for("rsc-kit.local-version-source");
|
|
@@ -278,6 +406,11 @@ export function installBackendVersionSource(source) {
|
|
|
278
406
|
}
|
|
279
407
|
/** The source in use: the app's, else the backend's, else this process's own. */
|
|
280
408
|
export function versionSource() {
|
|
409
|
+
// A factory, made into the store on first use: a store made in register()
|
|
410
|
+
// would make its database client in the build too, which runs register()
|
|
411
|
+
// without the database's settings.
|
|
412
|
+
if (typeof globals[SOURCE] === "function")
|
|
413
|
+
globals[SOURCE] = globals[SOURCE]();
|
|
281
414
|
const chosen = globals[SOURCE] ??
|
|
282
415
|
globals[BACKEND_SOURCE];
|
|
283
416
|
if (chosen)
|
|
@@ -295,7 +428,7 @@ export function versionSource() {
|
|
|
295
428
|
"instance, or in a worker, will not reach the tabs this one serves. Install a store every " +
|
|
296
429
|
"process shares with installVersionSource() in instrumentation.ts - sqlVersions for MySQL " +
|
|
297
430
|
"or SQLite, postgresVersions for Postgres, createVersions for anything else: " +
|
|
298
|
-
"https://docs.rsc-kit.dev/guides/
|
|
431
|
+
"https://docs.rsc-kit.dev/guides/live-data#where-the-versions-live. For a deploy that is one " +
|
|
299
432
|
"instance, installVersionSource(memoryVersions()) says so.");
|
|
300
433
|
}
|
|
301
434
|
}
|
|
@@ -459,6 +592,26 @@ function observe(name, version) {
|
|
|
459
592
|
if (stream.names.has(name))
|
|
460
593
|
stream.send(name, version);
|
|
461
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* A stream gone: and with it every name no other stream holds, which this
|
|
597
|
+
* process has no reason to remember. Kept, the latest version of every name
|
|
598
|
+
* any tab ever watched piled up for as long as the server ran.
|
|
599
|
+
*/
|
|
600
|
+
function close(stream) {
|
|
601
|
+
if (!streams.delete(stream))
|
|
602
|
+
return;
|
|
603
|
+
for (const name of stream.names) {
|
|
604
|
+
let held = false;
|
|
605
|
+
for (const other of streams) {
|
|
606
|
+
if (other.names.has(name)) {
|
|
607
|
+
held = true;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (!held)
|
|
612
|
+
known.delete(name);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
462
615
|
/** Mapped to this process's set of streams: what to ask about, relative to what is known. */
|
|
463
616
|
function sinceAll() {
|
|
464
617
|
const since = {};
|
|
@@ -467,11 +620,84 @@ function sinceAll() {
|
|
|
467
620
|
since[name] = known.get(name) ?? 0;
|
|
468
621
|
return since;
|
|
469
622
|
}
|
|
623
|
+
// ── Waking ───────────────────────────────────────────────────────────────────
|
|
624
|
+
//
|
|
625
|
+
// A backend that cannot hold a question open - PHP - is asked again on an
|
|
626
|
+
// interval. Something that can say "a version moved" the moment it does -
|
|
627
|
+
// a broadcast, a Redis channel - makes that interval a safety net: the
|
|
628
|
+
// renderer asks the moment it is woken, and otherwise rarely.
|
|
629
|
+
const WAKERS = Symbol.for("rsc-kit.wakers");
|
|
630
|
+
function wakers() {
|
|
631
|
+
return (globals[WAKERS] ??= {
|
|
632
|
+
listens: [],
|
|
633
|
+
started: false,
|
|
634
|
+
pending: false,
|
|
635
|
+
sleepers: new Set(),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
export function wakeOn(listen) {
|
|
639
|
+
const state = wakers();
|
|
640
|
+
state.listens.push(listen);
|
|
641
|
+
if (state.started)
|
|
642
|
+
start(listen);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Start one listener; one that fails - a database not up yet - is tried
|
|
646
|
+
* again, backing off to thirty seconds. Until it is up the timer stands in.
|
|
647
|
+
*/
|
|
648
|
+
function start(listen, failures = 0) {
|
|
649
|
+
const health = {
|
|
650
|
+
deaf: (reason) => {
|
|
651
|
+
const state = wakers();
|
|
652
|
+
const at = state.listens.indexOf(listen);
|
|
653
|
+
// No longer counted: the renderer goes back to asking on the shorter
|
|
654
|
+
// interval, as it does with nothing to wake it.
|
|
655
|
+
if (at === -1)
|
|
656
|
+
return;
|
|
657
|
+
state.listens.splice(at, 1);
|
|
658
|
+
console.warn("[rsc-kit] " + reason);
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
void Promise.resolve()
|
|
662
|
+
.then(() => listen(signal, health))
|
|
663
|
+
.catch((error) => {
|
|
664
|
+
if (failures === 0) {
|
|
665
|
+
console.warn("[rsc-kit] a wakeOn listener failed to start, trying again: " +
|
|
666
|
+
(error instanceof Error ? error.message : String(error)));
|
|
667
|
+
}
|
|
668
|
+
const retry = setTimeout(() => start(listen, failures + 1), Math.min(30_000, 1000 * 2 ** failures));
|
|
669
|
+
retry.unref?.();
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
/** Something said a version may have moved: ask now, and end any ask a store is holding. */
|
|
673
|
+
function signal() {
|
|
674
|
+
const state = wakers();
|
|
675
|
+
state.pending = true;
|
|
676
|
+
wake?.();
|
|
677
|
+
for (const sleeper of [...state.sleepers])
|
|
678
|
+
sleeper();
|
|
679
|
+
}
|
|
680
|
+
function startWakers() {
|
|
681
|
+
const state = wakers();
|
|
682
|
+
if (state.started)
|
|
683
|
+
return;
|
|
684
|
+
state.started = true;
|
|
685
|
+
// Set in the environment, not guessed: a broadcast server to listen to.
|
|
686
|
+
const url = env("RSC_BROADCAST_URL");
|
|
687
|
+
const key = env("RSC_BROADCAST_KEY");
|
|
688
|
+
if (url && key)
|
|
689
|
+
state.listens.push(listenToBroadcast({ url, key }));
|
|
690
|
+
for (const listen of state.listens)
|
|
691
|
+
start(listen);
|
|
692
|
+
}
|
|
470
693
|
async function loop() {
|
|
471
694
|
running = true;
|
|
695
|
+
startWakers();
|
|
472
696
|
try {
|
|
473
697
|
while (streams.size > 0) {
|
|
474
698
|
const started = Date.now();
|
|
699
|
+
const state = wakers();
|
|
700
|
+
state.pending = false;
|
|
475
701
|
try {
|
|
476
702
|
const moved = await versionSource().changed(sinceAll(), CHANGES_WAIT_MS);
|
|
477
703
|
for (const [name, version] of Object.entries(moved))
|
|
@@ -486,8 +712,11 @@ async function loop() {
|
|
|
486
712
|
}
|
|
487
713
|
}
|
|
488
714
|
// A source that waited used the interval; one that answered at once is
|
|
489
|
-
// asked again after it
|
|
490
|
-
|
|
715
|
+
// asked again after it - or, with something to wake it, only as a
|
|
716
|
+
// safety net. A stream opening, or a wake, ends the wait early; a wake
|
|
717
|
+
// that came during the ask is answered at once.
|
|
718
|
+
const interval = state.listens.length > 0 ? LISTENING_POLL_MS : CHANGES_INTERVAL_MS;
|
|
719
|
+
const remaining = state.pending ? 0 : interval - (Date.now() - started);
|
|
491
720
|
if (remaining > 0 && streams.size > 0) {
|
|
492
721
|
await new Promise((resolve) => {
|
|
493
722
|
const timer = setTimeout(resolve, remaining);
|
|
@@ -579,7 +808,7 @@ export async function changes(request) {
|
|
|
579
808
|
streams.add(me);
|
|
580
809
|
const end = () => {
|
|
581
810
|
clearInterval(keepalive);
|
|
582
|
-
|
|
811
|
+
close(me);
|
|
583
812
|
try {
|
|
584
813
|
sink.close();
|
|
585
814
|
}
|
|
@@ -614,7 +843,7 @@ export async function changes(request) {
|
|
|
614
843
|
},
|
|
615
844
|
cancel() {
|
|
616
845
|
if (stream)
|
|
617
|
-
|
|
846
|
+
close(stream);
|
|
618
847
|
},
|
|
619
848
|
});
|
|
620
849
|
return new Response(body, {
|
|
@@ -628,7 +857,12 @@ export async function changes(request) {
|
|
|
628
857
|
});
|
|
629
858
|
}
|
|
630
859
|
/** For tests: forget every stream and version this process has seen. */
|
|
860
|
+
/** For tests: how many names this process remembers for its open streams. */
|
|
861
|
+
export function rememberedNames() {
|
|
862
|
+
return known.size;
|
|
863
|
+
}
|
|
631
864
|
export function resetChanges() {
|
|
865
|
+
delete globals[WAKERS];
|
|
632
866
|
streams.clear();
|
|
633
867
|
known.clear();
|
|
634
868
|
warned = false;
|