Make API and RSS refresh notifications change-only
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- API and RSS refresh jobs now notify affected player screens after a successful update, so API-backed slides pick up new data without waiting for a manual page reload.
|
- API and RSS refresh jobs now notify affected player screens only when the refreshed data actually changes, so unchanged polls no longer trigger redundant player refreshes.
|
||||||
|
|
||||||
## 2.6.22 - 2026-08-09
|
## 2.6.22 - 2026-08-09
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,43 @@ async function notifyAffectedScreens(connection, common, notifyPlayerScreens, sl
|
|||||||
await notifyPlayerScreens(slugs, 'refresh');
|
await notifyPlayerScreens(slugs, 'refresh');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSnapshotValue(value) {
|
||||||
|
return String(value || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hasRssFeedChanged(connection, rssFeedId, items) {
|
||||||
|
const [rows] = await connection.query(
|
||||||
|
`SELECT item_json
|
||||||
|
FROM i_rss_feed_items
|
||||||
|
WHERE rss_feed_id = ?
|
||||||
|
ORDER BY position ASC, id ASC`,
|
||||||
|
[rssFeedId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentSnapshots = (rows || []).map(function (row) {
|
||||||
|
return normalizeSnapshotValue(row && row.item_json);
|
||||||
|
});
|
||||||
|
const nextSnapshots = (Array.isArray(items) ? items : []).map(function (item) {
|
||||||
|
return normalizeSnapshotValue(JSON.stringify(item || {}));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentSnapshots.length !== nextSnapshots.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < currentSnapshots.length; index += 1) {
|
||||||
|
if (currentSnapshots[index] !== nextSnapshots[index]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasApiSourceChanged(apiSource, responseDetails) {
|
||||||
|
return normalizeSnapshotValue(apiSource && apiSource.last_response_json) !== normalizeSnapshotValue(responseDetails && responseDetails.responseJson);
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshApiSource(pool, common, apiSourceOrId, actorId, notifyPlayerScreens) {
|
async function refreshApiSource(pool, common, apiSourceOrId, actorId, notifyPlayerScreens) {
|
||||||
const connection = await pool.getConnection();
|
const connection = await pool.getConnection();
|
||||||
try {
|
try {
|
||||||
@@ -97,7 +134,7 @@ async function refreshApiSource(pool, common, apiSourceOrId, actorId, notifyPlay
|
|||||||
);
|
);
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
if (!pullError) {
|
if (!pullError && hasApiSourceChanged(apiSource, responseDetails)) {
|
||||||
try {
|
try {
|
||||||
await notifyAffectedScreens(connection, common, notifyPlayerScreens, 'source_id', apiSource.id);
|
await notifyAffectedScreens(connection, common, notifyPlayerScreens, 'source_id', apiSource.id);
|
||||||
} catch (notifyError) {
|
} catch (notifyError) {
|
||||||
@@ -129,12 +166,13 @@ async function refreshRssFeed(pool, common, rssFeedId, feedUrl, itemLimit, actor
|
|||||||
}
|
}
|
||||||
|
|
||||||
await connection.beginTransaction();
|
await connection.beginTransaction();
|
||||||
|
const rssFeedChanged = await hasRssFeedChanged(connection, rssFeedId, updatedItems);
|
||||||
if (typeof common.replaceRssFeedItems === 'function') {
|
if (typeof common.replaceRssFeedItems === 'function') {
|
||||||
await common.replaceRssFeedItems(connection, rssFeedId, updatedItems);
|
await common.replaceRssFeedItems(connection, rssFeedId, updatedItems);
|
||||||
}
|
}
|
||||||
await connection.commit();
|
await connection.commit();
|
||||||
|
|
||||||
if (!pullError) {
|
if (!pullError && rssFeedChanged) {
|
||||||
try {
|
try {
|
||||||
await notifyAffectedScreens(connection, common, notifyPlayerScreens, 'feed_id', rssFeedId);
|
await notifyAffectedScreens(connection, common, notifyPlayerScreens, 'feed_id', rssFeedId);
|
||||||
} catch (notifyError) {
|
} catch (notifyError) {
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const { refreshApiSource, refreshRssFeed } = require('../src/web/lib/data-source-refresh');
|
||||||
|
|
||||||
|
function createConnection(options) {
|
||||||
|
const state = Object.assign({
|
||||||
|
rssItemSnapshots: [],
|
||||||
|
slideRows: [],
|
||||||
|
screenRows: []
|
||||||
|
}, options || {});
|
||||||
|
const calls = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
calls,
|
||||||
|
async beginTransaction() {
|
||||||
|
calls.push(['beginTransaction']);
|
||||||
|
},
|
||||||
|
async commit() {
|
||||||
|
calls.push(['commit']);
|
||||||
|
},
|
||||||
|
async rollback() {
|
||||||
|
calls.push(['rollback']);
|
||||||
|
},
|
||||||
|
release() {
|
||||||
|
calls.push(['release']);
|
||||||
|
},
|
||||||
|
async query(sql, params) {
|
||||||
|
calls.push(['query', sql, params]);
|
||||||
|
|
||||||
|
if (sql.includes('SELECT item_json')) {
|
||||||
|
return [state.rssItemSnapshots.map(function (itemJson) {
|
||||||
|
return { item_json: itemJson };
|
||||||
|
})];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sql.includes('SELECT id, content_json FROM c_slides')) {
|
||||||
|
return [state.slideRows];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sql.includes('SELECT DISTINCT s.slug')) {
|
||||||
|
return [state.screenRows];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [[]];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('refreshApiSource notifies players only when the API snapshot changes', async () => {
|
||||||
|
const connection = createConnection({
|
||||||
|
slideRows: [{ id: 10, content_json: JSON.stringify({ source_id: 7 }) }],
|
||||||
|
screenRows: [{ slug: 'screen-a' }]
|
||||||
|
});
|
||||||
|
const notifyCalls = [];
|
||||||
|
const pool = {
|
||||||
|
async getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const common = {
|
||||||
|
async fetchApiSourceResponse() {
|
||||||
|
return {
|
||||||
|
responseJson: JSON.stringify({ value: 'new' }, null, 2),
|
||||||
|
responseStatus: 200,
|
||||||
|
responseContentType: 'application/json'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
parseJsonSafe(value) {
|
||||||
|
return JSON.parse(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await refreshApiSource(pool, common, {
|
||||||
|
id: 7,
|
||||||
|
last_response_json: JSON.stringify({ value: 'old' }, null, 2)
|
||||||
|
}, 42, async function (slugs, payload) {
|
||||||
|
notifyCalls.push({ slugs, payload });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(notifyCalls, [{ slugs: ['screen-a'], payload: 'refresh' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refreshApiSource skips notifications when the API snapshot is unchanged', async () => {
|
||||||
|
const connection = createConnection({
|
||||||
|
slideRows: [{ id: 10, content_json: JSON.stringify({ source_id: 7 }) }],
|
||||||
|
screenRows: [{ slug: 'screen-a' }]
|
||||||
|
});
|
||||||
|
const notifyCalls = [];
|
||||||
|
const pool = {
|
||||||
|
async getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const snapshot = JSON.stringify({ value: 'same' }, null, 2);
|
||||||
|
const common = {
|
||||||
|
async fetchApiSourceResponse() {
|
||||||
|
return {
|
||||||
|
responseJson: snapshot,
|
||||||
|
responseStatus: 200,
|
||||||
|
responseContentType: 'application/json'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
parseJsonSafe(value) {
|
||||||
|
return JSON.parse(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await refreshApiSource(pool, common, {
|
||||||
|
id: 7,
|
||||||
|
last_response_json: snapshot
|
||||||
|
}, 42, async function (slugs, payload) {
|
||||||
|
notifyCalls.push({ slugs, payload });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(notifyCalls, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refreshRssFeed notifies players only when the RSS items change', async () => {
|
||||||
|
const connection = createConnection({
|
||||||
|
rssItemSnapshots: [JSON.stringify({ title: 'Old item' })],
|
||||||
|
slideRows: [{ id: 11, content_json: JSON.stringify({ feed_id: 9 }) }],
|
||||||
|
screenRows: [{ slug: 'screen-b' }]
|
||||||
|
});
|
||||||
|
const notifyCalls = [];
|
||||||
|
const pool = {
|
||||||
|
async getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const common = {
|
||||||
|
async fetchRssFeedItems() {
|
||||||
|
return [{ title: 'New item' }];
|
||||||
|
},
|
||||||
|
async replaceRssFeedItems() {
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
parseJsonSafe(value) {
|
||||||
|
return JSON.parse(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await refreshRssFeed(pool, common, 9, 'https://example.com/feed.xml', 10, 42, async function (slugs, payload) {
|
||||||
|
notifyCalls.push({ slugs, payload });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(notifyCalls, [{ slugs: ['screen-b'], payload: 'refresh' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('refreshRssFeed skips notifications when the RSS items are unchanged', async () => {
|
||||||
|
const snapshot = JSON.stringify({ title: 'Same item' });
|
||||||
|
const connection = createConnection({
|
||||||
|
rssItemSnapshots: [snapshot],
|
||||||
|
slideRows: [{ id: 11, content_json: JSON.stringify({ feed_id: 9 }) }],
|
||||||
|
screenRows: [{ slug: 'screen-b' }]
|
||||||
|
});
|
||||||
|
const notifyCalls = [];
|
||||||
|
const pool = {
|
||||||
|
async getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const common = {
|
||||||
|
async fetchRssFeedItems() {
|
||||||
|
return [{ title: 'Same item' }];
|
||||||
|
},
|
||||||
|
async replaceRssFeedItems() {
|
||||||
|
return 1;
|
||||||
|
},
|
||||||
|
parseJsonSafe(value) {
|
||||||
|
return JSON.parse(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await refreshRssFeed(pool, common, 9, 'https://example.com/feed.xml', 10, 42, async function (slugs, payload) {
|
||||||
|
notifyCalls.push({ slugs, payload });
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(notifyCalls, []);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user