openvibers/openvibe.reviews Commits

Reviews tells search engines when a page changes (plan T11, IndexNow): with INDEXNOW_KEY set the key file is served at /<key>.txt and an indexable review or product page appearing, changing or going away pings the engines (its path and the sitemap); drafts, private and noindex pages never; unset, nothing is mounted or sent. A home-page performance budget runs in CI.

OP OpenVibers committed 29 Sept 2026 · 1db22ffb24 · 9 files, +188 −16


Changes

ddiff --git a/README.md b/README.md
iindex 370a102..d2539d4 100644
---- a/README.md
++++ b/README.md
@@@ -119,7 +119,9 @@ share is expressed as `ratingValue` 0–100 with `bestRating: 100` and the real
(no live signal → `unsourced`, summary under the word minimum → `thin`, unsupported points →
`unsupported_claims`, unreviewed AI → hidden); sitemaps list indexable entities only, Atom/JSON feeds
list published summaries, `robots.txt` and `llms.txt` are served, and Search receives
--`reviews.index_document.upserted|deleted` with Sources items as provenance.
++`reviews.index_document.upserted|deleted` with Sources items as provenance. With `INDEXNOW_KEY` set,
++publishing, editing, unpublishing or deleting an indexable entity page tells IndexNow (the key file is
++served at `/<key>.txt`; unset, nothing is sent, and drafts and noindex pages never ping).
**Operations.** `/api/health`, `/api/ready` (DB required; Network key, Sources, Events relay, webhook
secret, Community and editors reported), `/release.json`, `/metrics` (outbox and backlog gauges).
ddiff --git a/server/app.js b/server/app.js
iindex 44e3f70..62f98f4 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -41,7 +41,7 @@ function lagged(read, initial) {
};
}
--function createApp({ config, svc, viewers, platform, sync, keys, db, log = console, rateLimits = true, fetchImpl = globalThis.fetch, limitsNow = null }) {
++function createApp({ config, svc, viewers, platform, sync, keys, db, log = console, rateLimits = true, fetchImpl = globalThis.fetch, limitsNow = null, indexnow = null }) {
const app = express();
app.disable('x-powered-by');
app.set('trust proxy', config.trustProxy);
@@@ -96,6 +96,9 @@ function createApp({ config, svc, viewers, platform, sync, keys, db, log = conso
app.use('/auth', createSessionRoutes({ config, viewers, log, fetchImpl }));
{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'reviews', service: 'reviews', host: 'openvibe.reviews', name: 'OpenVibe.Reviews', profile: 'ugc' })); }
++ // GET /<key>.txt — the IndexNow key file (mounted only when a key is configured; it serves itself).
++ if (indexnow && indexnow.enabled) app.use(indexnow.keyFile);
++
// Signed deliveries from OpenVibe.Events. Loopback/internal only in nginx (not proxied publicly).
const consumer = consumerRouter({ db, svc, sync, config, log });
app.use('/internal', http.middleware(), consumer.router);
ddiff --git a/server/config.js b/server/config.js
iindex dc0c724..0daedac 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -32,7 +32,9 @@ function load(env = process.env) {
hour: Math.max(1, int(env.REVIEWS_LIMITS_HOUR, 3000)),
},
// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner role).
-- db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },
++ // Non-production without DATABASE_URL: the embedded PGlite database in data/pglite
++ // (REVIEWS_PGLITE_DIR, tests: an isolated directory of their own).
++ db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '', pgliteDir: env.REVIEWS_PGLITE_DIR || '' },
valkey: { url: env.VALKEY_URL || '', prefix: env.VALKEY_PREFIX || 'ov:reviews:' },
// Identity: OpenVibe.Network signs user JWTs (SSO) and service tokens (client credentials).
@@@ -55,6 +57,11 @@ function load(env = process.env) {
},
cookies: { secure: env.COOKIE_SECURE ? env.COOKIE_SECURE === 'true' : isProduction },
++ // IndexNow (openvibe-shared/indexnow): a key makes search engines recrawl an entity page the
++ // moment it appears, changes or leaves the index (the key file is served at /<key>.txt). Unset →
++ // off: no key file, nothing sent. Tests and drills never set it.
++ indexnow: { key: String(env.INDEXNOW_KEY || '').trim() },
++
// Other services (all optional: each integration degrades to an explicit failure state).
eventsUrl: trim(env.EVENTS_URL || ''),
eventsRelayIntervalMs: int(env.EVENTS_RELAY_INTERVAL_MS, 2000),
ddiff --git a/server/db.js b/server/db.js
iindex fcbc2f4..1dcc66f 100644
---- a/server/db.js
++++ b/server/db.js
@@@ -47,14 +47,16 @@ const DEV_PGLITE = path.join(__dirname, '..', 'data', 'pglite');
/**
* The serving handle (ADR-035): DATABASE_URL through PgBouncer; in development without it, an embedded PGlite database
-- * in data/pglite. Migrations run first, as the owner (DATABASE_DIRECT_URL), or on the embedded handle.
++ * in data/pglite (REVIEWS_PGLITE_DIR overrides the directory — how a test gets its own). Migrations run first, as the
++ * owner (DATABASE_DIRECT_URL), or on the embedded handle.
*/
async function openDb(config, { log = console, registry } = {}) {
if (!config.db.url) {
if (config.isProduction) throw new Error('DATABASE_URL is not set: production serves from PostgreSQL (OpenVibe.Host roles/data add-service.sh reviews)');
-- log.warn(`[Reviews] DATABASE_URL unset: embedded PGlite database in ${DEV_PGLITE} (development only, one process)`);
-- fs.mkdirSync(DEV_PGLITE, { recursive: true });
-- const db = createDb({ pglite: DEV_PGLITE, service: 'reviews', registry, log });
++ const dir = config.db.pgliteDir || DEV_PGLITE;
++ log.warn(`[Reviews] DATABASE_URL unset: embedded PGlite database in ${dir} (development only, one process)`);
++ fs.mkdirSync(dir, { recursive: true });
++ const db = createDb({ pglite: dir, service: 'reviews', registry, log });
await db.migrate({ dir: MIGRATIONS, log });
return db;
}
ddiff --git a/server/index.js b/server/index.js
iindex 2e20517..3c32605 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -16,19 +16,23 @@ const { createSourcesSync } = require('./reviews/sync');
const { createKeyStore } = require('./auth/keys');
const { createViewerResolver } = require('./auth/viewer');
const { createApp } = require('./app');
++const { createIndexNow } = require('openvibe-shared/indexnow');
--async function start({ config, db: givenDb = null, now = () => Date.now(), fetchImpl = globalThis.fetch, tokens = null, publicKey = null, log = console, listen = true, workers = listen, rateLimits = true, limitsNow = null } = {}) {
++async function start({ config, db: givenDb = null, now = () => Date.now(), fetchImpl = globalThis.fetch, tokens = null, publicKey = null, log = console, listen = true, workers = listen, rateLimits = true, limitsNow = null, indexnow: givenIndexnow = undefined } = {}) {
config = config || load();
// PostgreSQL (ADR-035): opened and migrated here unless the caller (a test) hands in a migrated handle.
const db = givenDb || await openDb(config, { log });
const stores = createStores(db, { now });
const platform = createPlatform({ config, db, fetchImpl, tokens, now, log });
-- const svc = createReviewsService({ db, stores, outbox: platform.outbox, config, now, log });
++ // IndexNow (openvibe-shared/indexnow): created once at boot from INDEXNOW_KEY. Unset → off, nothing
++ // mounted, nothing sent; tests and drills never set it (nor hand one in).
++ const indexnow = givenIndexnow !== undefined ? givenIndexnow : createIndexNow({ host: config.baseUrl, key: config.indexnow.key, fetch: fetchImpl, log });
++ const svc = createReviewsService({ db, stores, outbox: platform.outbox, config, now, log, indexnow });
const sync = createSourcesSync({ db, svc, platform, config, now, log });
const keys = createKeyStore({ config, fetchImpl, log, publicKey });
keys.ensure().catch(() => {});
const viewers = createViewerResolver({ keys, config });
-- const app = createApp({ config, svc, viewers, platform, sync, keys, db, log, rateLimits, fetchImpl, limitsNow });
++ const app = createApp({ config, svc, viewers, platform, sync, keys, db, log, rateLimits, fetchImpl, limitsNow, indexnow });
// Search holds whatever the current rules say (idempotent: unchanged documents are not re-sent).
try { await svc.reconcileIndex(); } catch (err) { log.warn(`[Reviews] index reconcile: ${err.message}`); }
@@@ -58,7 +62,7 @@ async function start({ config, db: givenDb = null, now = () => Date.now(), fetch
if (server) await new Promise((resolve) => server.close(resolve));
if (!givenDb) await db.close();
}
-- return { app, db, svc, sync, stores, platform, keys, viewers, server, config, stop };
++ return { app, db, svc, sync, stores, platform, keys, viewers, indexnow, server, config, stop };
}
module.exports = { start };
ddiff --git a/server/reviews/service.js b/server/reviews/service.js
iindex 28d5d0a..db2383a 100644
---- a/server/reviews/service.js
++++ b/server/reviews/service.js
@@@ -55,7 +55,7 @@ const RATING_TEXT_RE = new RegExp([
const toIso = (ms) => (ms == null ? null : new Date(ms).toISOString());
--function createReviewsService({ db, stores, outbox, config, now = () => Date.now(), log = console }) {
++function createReviewsService({ db, stores, outbox, config, now = () => Date.now(), log = console, indexnow = null }) {
const { revisions, reviews, redirects, discussions, sequencer } = stores;
const access = createAccess(config);
const origin = config.baseUrl;
@@@ -158,6 +158,10 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
const tx = async (fn) => await db.tx(async () => await fn()); // ambient: plain db calls inside join it
const entityPath = (e) => `/e/${encodeURIComponent(e.slug)}`;
const entityUrl = (e) => seo.canonicalUrl(origin, entityPath(e));
++ const sitemapUrl = seo.canonicalUrl(origin, '/sitemap.xml');
++ // IndexNow (openvibe-shared/indexnow): tell the engines when a public, indexable entity page
++ // appears, changes or disappears. Never for drafts, private or noindex pages; off without a key.
++ const pinger = indexnow && indexnow.enabled ? indexnow : null;
const parse = (s, d) => { try { return s == null ? d : JSON.parse(s); } catch { return d; } };
function actorId(actor) {
@@@ -1108,6 +1112,7 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
return await tx(async () => {
const e = await entityOrFail(ref);
if (e.state !== 'active') fail(409, 'entity.not_active', `This entity is ${e.state}`);
++ const wasIndexable = pinger ? (await decide(e)).indexable : false;
const next = { ...e };
const changes = {};
if (name != null && name !== e.name) { next.name = checkName(name); changes.name = [e.name, next.name]; await insertAlias(e.id, 'name', next.name, actor); }
@@@ -1126,7 +1131,13 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
.run(next.name, next.kind, next.description, next.slug, next.noindex, now(), e.id);
if (Object.keys(changes).length) await audit(actor, 'entity.updated', e.id, null, changes);
await syncEntity(e.id);
-- return await q.entity.get(e.id);
++ const fresh = await q.entity.get(e.id);
++ // IndexNow: the old URL changed or left the index; the new one appeared or changed.
++ if (pinger) {
++ if (wasIndexable) pinger.pingSoon([entityUrl(e), sitemapUrl]);
++ if ((await decide(fresh)).indexable) pinger.pingSoon([entityUrl(fresh), sitemapUrl]);
++ }
++ return fresh;
});
},
@@@ -1135,12 +1146,15 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
return await tx(async () => {
const e = await entityOrFail(ref);
if (e.state !== 'active') fail(409, 'entity.not_active', `This entity is ${e.state}`);
++ const wasIndexable = pinger ? (await decide(e)).indexable : false;
if ((await q.mergedInto.all(e.id)).length) fail(409, 'entity.has_merges', 'Split the entities merged into this one first');
const active = await q.activeSignalsOfEntity.all(e.id);
if (active.length) fail(409, 'entity.has_signals', 'This entity has live signals: reattribute or ignore their items first');
await db.prepare("UPDATE review_entities SET state = 'deleted', deleted_at = ?, updated_at = ? WHERE id = ?").run(now(), now(), e.id);
await audit(actor, 'entity.deleted', e.id, null, { note });
await syncEntity(e.id);
++ // IndexNow: the page left the index (only if it was a public, indexable page).
++ if (pinger && wasIndexable) pinger.pingSoon([entityUrl(e), sitemapUrl]);
return await q.entity.get(e.id);
});
},
@@@ -1468,6 +1482,7 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
if (rev.meta && rev.meta.system && !(review && review.decision === 'approved')) fail(409, 'summary.review_required', 'A revision prepared after a source change is published only after an editor approves it');
const bad = (await citationState(e.id, rev)).filter((p) => !p.supported);
if (bad.length) fail(409, 'summary.unsupported_points', `${bad.length} point(s) cite no live signal of this entity`, { points: bad.map((p) => p.key) });
++ const wasIndexable = pinger ? (await decide(e)).indexable : false;
const wasPublished = summary.state === 'published';
const before = summary.published_revision;
await q.publishSummary.run({ id: summary.id, n, now: now() });
@@@ -1485,6 +1500,8 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
extra: { entity_id: e.id, entity_slug: fresh.slug, authorship: rec ? hooks.AUTHORSHIP[rec.mode] : null, ...(rev.meta && rev.meta.correction ? { correction: { note: rev.meta.correction.note, corrects: rev.meta.correction.corrects } } : {}) },
}));
}
++ // IndexNow: the entity page appeared or changed; never a draft or a non-indexable page.
++ if (pinger && (decision.indexable || wasIndexable)) pinger.pingSoon([entityUrl(fresh), sitemapUrl]);
return await q.summary.get(e.id);
});
},
@@@ -1495,13 +1512,17 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
const e = await entityOrFail(ref);
const summary = await q.summary.get(e.id);
if (!summary || summary.state !== 'published') fail(409, 'summary.not_published', 'Nothing is published');
++ const wasIndexable = pinger ? (await decide(e)).indexable : false;
await q.unpublishSummary.run(now(), summary.id);
await audit(actor, 'summary.unpublished', e.id, summary.id, { revision: summary.published_revision });
const doc = await syncEntity(e.id);
const fresh = await q.entity.get(e.id);
++ const decision = await decide(fresh);
++ // IndexNow: the page changed or left the index; only when it was or is a public, indexable page.
++ if (pinger && (wasIndexable || decision.indexable)) pinger.pingSoon([entityUrl(fresh), sitemapUrl]);
await emit(hooks.publicationEvent({
product: 'reviews', type: 'summary', action: 'unpublished', id: summary.id, revision: summary.published_revision,
-- actor: hooks.subjectRef(actorId(actor)), decision: await decide(fresh), now: now(),
++ actor: hooks.subjectRef(actorId(actor)), decision, now: now(),
document: { owner: 'reviews', type: 'summary', id: summary.id, revision: doc ? doc.revision : 0, deleted: true },
extra: { entity_id: e.id, entity_slug: fresh.slug },
}));
ddiff --git a/test/helpers.js b/test/helpers.js
iindex c670ee9..a7187c7 100644
---- a/test/helpers.js
++++ b/test/helpers.js
@@@ -147,7 +147,7 @@ function fakeCommunity() {
* (default false) turns on the per-address and per-actor limits; limitsNow is the per-actor limiter's
* clock; log is Reviews' logger (default quiet).
*/
--async function boot({ env = {}, sources = fakeSources(), community = null, now, workers = false, rateLimits = false, limitsNow = null, log = quiet } = {}) {
++async function boot({ env = {}, sources = fakeSources(), community = null, now, workers = false, rateLimits = false, limitsNow = null, log = quiet, indexnow } = {}) {
const config = load({
NODE_ENV: 'test', PORT: '0', HOST: '127.0.0.1', BASE_URL: 'http://reviews.test',
OV_NETWORK_URL: ISSUER, OV_NETWORK_INTERNAL_URL: '', REVIEWS_GATE_MIN_WORDS: '20',
@@@ -164,7 +164,7 @@ async function boot({ env = {}, sources = fakeSources(), community = null, now,
// One database per boot (PGlite, or REVIEWS_TEST_STORE=pg: the containers), dropped when the boot stops.
const testdb = await testDb();
const h = await start({
-- config, db: testdb.db, publicKey, log, listen: true, workers, rateLimits, limitsNow, now, fetchImpl,
++ config, db: testdb.db, publicKey, log, listen: true, workers, rateLimits, limitsNow, now, fetchImpl, indexnow,
tokens: { getToken: async () => 'stub-token', authHeaders: async () => ({ Authorization: 'Bearer stub-token' }), invalidate() {} },
});
const base = `http://127.0.0.1:${h.server.address().port}`;
ddiff --git a/test/indexnow.test.js b/test/indexnow.test.js
nnew file mode 100644
iindex 0000000..546575b
---- /dev/null
++++ b/test/indexnow.test.js
@@@ -0,0 +1,71 @@
++'use strict';
++/**
++ * IndexNow (openvibe-shared/indexnow): INDEXNOW_KEY unset → the feature is off (no key route, nothing
++ * sent). With a key, the key file is served at /<key>.txt as text/plain, and publishing an indexable
++ * entity page pings the engines with the entity path and the sitemap. Drafts never ping.
++ */
++const assert = require('assert');
++const { boot, req, suite, editorToken, steamItem, importItem, createEntity } = require('./helpers');
++
++const t = suite('indexnow');
++const KEY = 'k'.repeat(32);
++const OVERVIEW = 'Most sampled Steam reviewers recommend this game, and the sample here is broad enough to clear the thin-content gate for the entity page in this test without difficulty.';
++
++/** An entity with three live signals and their ids, resolvable through the steam source binding. */
++async function entityWithSignals(h) {
++ const e = await createEntity(h, { name: 'Portal 2', kind: 'game', aliases: [{ type: 'source', value: 'steam-reviews-portal-2' }] });
++ for (let i = 0; i < 3; i++) await importItem(h, steamItem({ votedUp: i < 2 }));
++ const sigs = (await h.db.prepare("SELECT id FROM review_signals WHERE entity_id = ? AND status = 'active' ORDER BY id").all(e.id)).map((s) => s.id);
++ return { e, sigs };
++}
++
++t('without a key IndexNow is off: no key route and nothing sent', async () => {
++ const h = await boot();
++ try {
++ assert.strictEqual(h.indexnow.enabled, false, 'IndexNow is off without a key');
++ const res = await req(h, 'GET', `/${KEY}.txt`);
++ assert.strictEqual(res.status, 404, res.text);
++ } finally { await h.stop(); }
++});
++
++t('with a key the key file answers text/plain with the key', async () => {
++ const h = await boot({ env: { INDEXNOW_KEY: KEY } });
++ try {
++ assert.strictEqual(h.indexnow.enabled, true);
++ const res = await req(h, 'GET', `/${KEY}.txt`);
++ assert.strictEqual(res.status, 200, res.text);
++ assert.match(res.headers.get('content-type'), /text\/plain/);
++ assert.strictEqual(res.text, KEY);
++ } finally { await h.stop(); }
++});
++
++t('a draft never pings; a publish pings the page path and the sitemap; an unpublish pings again', async () => {
++ // A spy in place of the module's HTTP send: records every pingSoon batch.
++ const pings = [];
++ const spy = {
++ enabled: true,
++ keyFile: (_req, _res, next) => next(),
++ pingSoon: (urls) => { const a = Array.isArray(urls) ? urls : [urls]; pings.push(...a); return a.length; },
++ ping: async () => ({ sent: 0, status: 0 }),
++ flush: async () => ({ sent: 0, status: 0 }),
++ };
++ const h = await boot({ indexnow: spy });
++ try {
++ const { e, sigs } = await entityWithSignals(h);
++
++ const draft = await req(h, 'POST', `/api/v1/entities/${e.slug}/summary/revisions`, { token: editorToken(), body: { overview: OVERVIEW, overview_signals: sigs } });
++ assert.strictEqual(draft.status, 201, draft.text);
++ assert.deepStrictEqual(pings, [], 'a draft must not ping');
++
++ const pub = await req(h, 'POST', `/api/v1/entities/${e.slug}/summary/revisions`, { token: editorToken(), body: { overview: OVERVIEW, overview_signals: sigs, publish: true } });
++ assert.strictEqual(pub.status, 201, pub.text);
++ assert.ok(pings.includes('http://reviews.test/e/portal-2'), JSON.stringify(pings));
++ assert.ok(pings.includes('http://reviews.test/sitemap.xml'), JSON.stringify(pings));
++
++ pings.length = 0;
++ const unpub = await req(h, 'POST', `/api/v1/entities/${e.slug}/summary/unpublish`, { token: editorToken() });
++ assert.strictEqual(unpub.status, 200, unpub.text);
++ assert.ok(pings.includes('http://reviews.test/e/portal-2'), JSON.stringify(pings));
++ assert.ok(pings.includes('http://reviews.test/sitemap.xml'), JSON.stringify(pings));
++ } finally { await h.stop(); }
++});
ddiff --git a/test/perf-budget.test.js b/test/perf-budget.test.js
nnew file mode 100644
iindex 0000000..c9d5003
---- /dev/null
++++ b/test/perf-budget.test.js
@@@ -0,0 +1,62 @@
++'use strict';
++// Size budgets for openvibe.reviews' home page (roadmap WS-T task 1, openvibe-shared/perf-budget): the
++// server as it runs (a fresh database), measured without a browser. Budgets sit a little above today's
++// measurement; raising one is a decision to state in the commit.
++// node test/perf-budget.test.js
++const assert = require('assert');
++const fs = require('fs');
++const net = require('net');
++const os = require('os');
++const path = require('path');
++const { spawn } = require('child_process');
++const { measure, check, format } = require('openvibe-shared/perf-budget');
++
++const BUDGETS = {
++ htmlRawKB: 20, // measured 17.8 (fresh database)
++ htmlBrotliKB: 5.5, // 4.6
++ jsFiles: 5, // 4
++ jsRawKB: 230, // 212.2
++ jsBrotliKB: 55, // 49.9
++ cssFiles: 2, // 1
++ cssRawKB: 7.5, // 6.7
++ cssBrotliKB: 2, // 1.7
++ externalFiles: 1, // 0
++};
++
++const freePort = () => new Promise((resolve) => { const s = net.createServer(); s.listen(0, '127.0.0.1', () => { const { port } = s.address(); s.close(() => resolve(port)); }); });
++
++(async () => {
++ // A database of its own (not the shared dev PGlite in data/pglite, and not whatever the caller's
++ // DATABASE_URL names), so the measurement is the server on a fresh database and the run leaves nothing.
++ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-budget-'));
++ const pgliteDir = path.join(dir, 'pglite');
++ const port = await freePort();
++ const child = spawn(process.execPath, [path.join(__dirname, '..', 'server', 'index.js')], {
++ cwd: path.join(__dirname, '..'),
++ env: {
++ ...process.env, PORT: String(port), HOST: '127.0.0.1', NODE_ENV: 'test',
++ DATABASE_URL: '', DATABASE_DIRECT_URL: '', VALKEY_URL: '', REVIEWS_PGLITE_DIR: pgliteDir,
++ },
++ stdio: ['ignore', 'ignore', 'pipe'],
++ });
++ let stderr = '';
++ child.stderr.on('data', (d) => { stderr = (stderr + d).slice(-2000); });
++ const base = `http://127.0.0.1:${port}`;
++ try {
++ let up = false;
++ for (let i = 0; i < 150 && !up; i++) {
++ up = await fetch(`${base}/api/health`).then((r) => r.ok).catch(() => false);
++ if (!up) await new Promise((r) => setTimeout(r, 100));
++ }
++ assert.ok(up, `the server did not start:\n${stderr}`);
++ assert.ok(fs.existsSync(pgliteDir), 'the server did not use the isolated database (REVIEWS_PGLITE_DIR)');
++ const m = await measure({ base });
++ const over = check(m, BUDGETS);
++ assert.deepStrictEqual(over, [], format(m, over));
++ console.log(format(m));
++ console.log('perf budget: all checks passed');
++ } finally {
++ child.kill();
++ fs.rmSync(dir, { recursive: true, force: true });
++ }
++})().catch((err) => { console.error(err); process.exitCode = 1; });