// Per-user push-notification subscriptions (which streams a user opted into; // applied to every device they register). The catalog is core's plus every // installed module's, so it is read back through modules/registries rather than // from a config file (MODULE_SYSTEM.md §1.8). // // **This is now the push PROJECTION of `notification_channel_prefs`** // (ENGAGEMENT.md Phase 3), and it keeps its exact wire shape on purpose: the // shipped Android client's DTO is `{ streams: [...] }` and cannot be changed from // this side. So the general table gained the channel dimension and this one stays // the answer to "which streams does this user want pushed" — the only question // that client knows how to ask. Every write here fans out to there; every write // there that touches push fans out to here. What `utils/pushDispatch` reads did // not change at all, which is what makes this phase touch no delivery path. const db = require('./notificationSubs.db') const { isValidStream } = require('../../modules/registries') const getForUser = async (userId) => (await db.listByUser(userId)).map((r) => r.stream_id) // Replace the user's subscription set. Ignores unknown ids and de-dupes, so a // stale client can't create rows for streams that no longer exist. Returns the // stored (cleaned) set. // // The mirror is required lazily rather than at the top of the file: the channel // prefs model requires the registries and the channel registry, and this module // is required by the router at boot. A cycle here would be a silent half-loaded // module rather than an error, and there is nothing to gain from the eager form. async function setForUser(userId, streams) { const clean = [...new Set((Array.isArray(streams) ? streams : []).filter(isValidStream))] await db.replaceForUser(userId, clean) // eslint-disable-next-line global-require await require('../notificationChannelPrefs/notificationChannelPrefs.model').mirrorPushSet(userId, clean) return clean } module.exports = { getForUser, setForUser }