openvibers/openvibe.reviews Commits
The home's 'with data' list reads from the signals to the entities: 178 ms to 2.5 ms (#195)
Changes
@@@ -2050,9 +2050,14 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now/** Entities with at least one live signal or a published summary (the home page). */async entitiesWithData(limit = 60) {-- return (await Promise.all((await db.prepare(`SELECT e.* FROM review_entities e WHERE e.state = 'active' AND (-- EXISTS (SELECT 1 FROM review_signals s JOIN review_entities m ON m.id = s.entity_id WHERE s.status = 'active' AND (m.id = e.id OR m.merged_into = e.id))-- OR EXISTS (SELECT 1 FROM review_summaries u WHERE u.entity_id = e.id AND u.state = 'published'))++ // From the few entities that have data to the entities, not the other way round: the EXISTS form ran its++ // subquery once per active entity (6,921 on 2026-11-02, 178 ms of the home); this one reads the active++ // signals (an entity's own, or one merged into it) and published summaries first (2.5 ms, the same rows).++ return (await Promise.all((await db.prepare(`WITH has AS (++ SELECT s.entity_id AS id FROM review_signals s WHERE s.status = 'active'++ UNION SELECT m.merged_into FROM review_signals s JOIN review_entities m ON m.id = s.entity_id WHERE s.status = 'active' AND m.merged_into IS NOT NULL++ UNION SELECT u.entity_id FROM review_summaries u WHERE u.state = 'published')++ SELECT e.* FROM review_entities e JOIN has h ON h.id = e.id WHERE e.state = 'active'ORDER BY e.updated_at DESC LIMIT ?`).all(limit)).map(async (e) => ({ entity: e, aggregate: aggregateView(await q.lastAggregate.get(e.id)) }))));},nnew file mode 100644@@@ -0,0 +1,40 @@++'use strict';++/**++ * The home's "with data" list (svc.entitiesWithData): every active entity with an active signal of its own, or one on++ * an entity merged into it, or a published summary; none without; newest change first. It reads from the signals and++ * summaries to the entities (2.5 ms on production), not a subquery per active entity (178 ms, 2026-11-02).++ */++const assert = require('assert');++const { boot, suite, steamItem, importItem, createEntity, req, editorToken } = require('./helpers');++++const t = suite('entities-with-data');++++t('the entities with signals, merged signals or a published summary; none without', async () => {++ const h = await boot();++ try {++ const rated = await createEntity(h, { name: 'Portal 2', kind: 'game', aliases: [{ type: 'source', value: 'steam-reviews-portal-2' }] });++ const dup = await createEntity(h, { name: 'Portal Two', kind: 'game', aliases: [{ type: 'source', value: 'steam-reviews-portal-two-dup' }] });++ const keeper = await createEntity(h, { name: 'Portal (the keeper)', kind: 'game' });++ const bare = await createEntity(h, { name: 'No data yet', kind: 'game' });++ const summarised = await createEntity(h, { name: 'Summarised', kind: 'game' });++ await importItem(h, steamItem({ votedUp: true }));++ await importItem(h, steamItem({ votedUp: false, source: 'steam-reviews-portal-two-dup' }));++ // The duplicate merges into the keeper: its signal now counts for the keeper.++ const m = await req(h, 'POST', `/api/v1/entities/${dup.slug}/merge`, { token: editorToken(), body: { into: keeper.slug, note: 'duplicate' } });++ assert.ok(m.status < 300, m.text);++ await h.db.prepare("INSERT INTO review_summaries (id, entity_id, state, created_at, updated_at) VALUES (?, ?, 'published', ?, ?)").run('sum_entities_with_data', summarised.id, Date.now(), Date.now());++ const got = (await h.svc.entitiesWithData(60)).map((x) => x.entity.id);++ assert.ok(got.includes(rated.id), 'its own active signal');++ assert.ok(got.includes(keeper.id), 'a signal on an entity merged into it');++ assert.ok(!got.includes(dup.id), 'the merged entity itself is not active');++ assert.ok(!got.includes(bare.id), 'nothing without data');++ assert.ok(got.includes(summarised.id), 'a published summary');++ const rows = await h.db.prepare('SELECT id, updated_at FROM review_entities WHERE id = ANY(?)').all(got);++ const at = new Map(rows.map((r) => [r.id, String(r.updated_at)]));++ for (let i = 1; i < got.length; i++) assert.ok(at.get(got[i - 1]) >= at.get(got[i]), 'newest change first');++ assert.ok((await h.svc.entitiesWithData(1)).length === 1, 'the limit holds');++ } finally {++ await h.stop();++ }++});++
@@ -2050,9 +2050,14 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
@@ -2050,9 +2050,14 @@ function createReviewsService({ db, stores, outbox, config, now = () => Date.now
/** Entities with at least one live signal or a published summary (the home page). */
/** Entities with at least one live signal or a published summary (the home page). */
async entitiesWithData(limit = 60) {
async entitiesWithData(limit = 60) {
- return (await Promise.all((await db.prepare(`SELECT e.* FROM review_entities e WHERE e.state = 'active' AND (
- EXISTS (SELECT 1 FROM review_signals s JOIN review_entities m ON m.id = s.entity_id WHERE s.status = 'active' AND (m.id = e.id OR m.merged_into = e.id))
- OR EXISTS (SELECT 1 FROM review_summaries u WHERE u.entity_id = e.id AND u.state = 'published'))
+ // From the few entities that have data to the entities, not the other way round: the EXISTS form ran its
+ // subquery once per active entity (6,921 on 2026-11-02, 178 ms of the home); this one reads the active
+ // signals (an entity's own, or one merged into it) and published summaries first (2.5 ms, the same rows).
+ return (await Promise.all((await db.prepare(`WITH has AS (
+ SELECT s.entity_id AS id FROM review_signals s WHERE s.status = 'active'
+ UNION SELECT m.merged_into FROM review_signals s JOIN review_entities m ON m.id = s.entity_id WHERE s.status = 'active' AND m.merged_into IS NOT NULL
+ UNION SELECT u.entity_id FROM review_summaries u WHERE u.state = 'published')
+ SELECT e.* FROM review_entities e JOIN has h ON h.id = e.id WHERE e.state = 'active'
ORDER BY e.updated_at DESC LIMIT ?`).all(limit)).map(async (e) => ({ entity: e, aggregate: aggregateView(await q.lastAggregate.get(e.id)) }))));
ORDER BY e.updated_at DESC LIMIT ?`).all(limit)).map(async (e) => ({ entity: e, aggregate: aggregateView(await q.lastAggregate.get(e.id)) }))));
},
},
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
+'use strict';
+/**
+ * The home's "with data" list (svc.entitiesWithData): every active entity with an active signal of its own, or one on
+ * an entity merged into it, or a published summary; none without; newest change first. It reads from the signals and
+ * summaries to the entities (2.5 ms on production), not a subquery per active entity (178 ms, 2026-11-02).
+ */
+const assert = require('assert');
+const { boot, suite, steamItem, importItem, createEntity, req, editorToken } = require('./helpers');
+
+const t = suite('entities-with-data');
+
+t('the entities with signals, merged signals or a published summary; none without', async () => {
+ const h = await boot();
+ try {
+ const rated = await createEntity(h, { name: 'Portal 2', kind: 'game', aliases: [{ type: 'source', value: 'steam-reviews-portal-2' }] });
+ const dup = await createEntity(h, { name: 'Portal Two', kind: 'game', aliases: [{ type: 'source', value: 'steam-reviews-portal-two-dup' }] });
+ const keeper = await createEntity(h, { name: 'Portal (the keeper)', kind: 'game' });
+ const bare = await createEntity(h, { name: 'No data yet', kind: 'game' });
+ const summarised = await createEntity(h, { name: 'Summarised', kind: 'game' });
+ await importItem(h, steamItem({ votedUp: true }));
+ await importItem(h, steamItem({ votedUp: false, source: 'steam-reviews-portal-two-dup' }));
+ // The duplicate merges into the keeper: its signal now counts for the keeper.
+ const m = await req(h, 'POST', `/api/v1/entities/${dup.slug}/merge`, { token: editorToken(), body: { into: keeper.slug, note: 'duplicate' } });
+ assert.ok(m.status < 300, m.text);
+ await h.db.prepare("INSERT INTO review_summaries (id, entity_id, state, created_at, updated_at) VALUES (?, ?, 'published', ?, ?)").run('sum_entities_with_data', summarised.id, Date.now(), Date.now());
+ const got = (await h.svc.entitiesWithData(60)).map((x) => x.entity.id);
+ assert.ok(got.includes(rated.id), 'its own active signal');
+ assert.ok(got.includes(keeper.id), 'a signal on an entity merged into it');
+ assert.ok(!got.includes(dup.id), 'the merged entity itself is not active');
+ assert.ok(!got.includes(bare.id), 'nothing without data');
+ assert.ok(got.includes(summarised.id), 'a published summary');
+ const rows = await h.db.prepare('SELECT id, updated_at FROM review_entities WHERE id = ANY(?)').all(got);
+ const at = new Map(rows.map((r) => [r.id, String(r.updated_at)]));
+ for (let i = 1; i < got.length; i++) assert.ok(at.get(got[i - 1]) >= at.get(got[i]), 'newest change first');
+ assert.ok((await h.svc.entitiesWithData(1)).length === 1, 'the limit holds');
+ } finally {
+ await h.stop();
+ }
+});
+