// ── Shard event → push fan-out ───────────────────────────────────────────── // // MODULE-UO CONTENT, still living in core — the inverted half of // MODULE_SYSTEM.md §1.8's second entangled file. `utils/pushDispatch.js` is core // infrastructure, but its `fromShardEvent()` required the shardLinks model and // the shard event mapper, which is a core file importing content. PR 4 inverted // it: `publish()` stays core, and this — the thing that knows what a shard event // is — moved out to call it. Phase 3 moves this file to module-uo whole, where it // will reach `publish` through `ctx.push.publish` instead of a require. // // Owner resolution is the reason this cannot just be a mapper: a personal // (owner-keyed) target names a GAME account, and turning that into a website user // needs the shardLinks model. An unlinked account is simply nobody to notify. const shardLinks = require('../model/shardLinks/shardLinks.model') const { mapShardEvent } = require('../config/shardStreams') const { push } = require('../core') const { publish } = push const log = require('../core').logger('shard-push') // Fan a shard event out to push. Resolves personal (owner-keyed) targets to the // owning website user via shardLinks (an unlinked account → nobody to notify). // Never throws — a dead relay must never affect ingest. async function fromShardEvent(event, deps = {}) { const links = deps.shardLinks || shardLinks const doPublish = deps.publish || publish const targets = mapShardEvent(event, deps.tracker) for (const t of targets) { try { if (t.ownerAccount) { let owner = null try { owner = await links.getByAccount(t.ownerAccount) } catch { owner = null } if (!owner || owner.userId == null) continue await doPublish(t.streamId, { ref: t.ref, ownerUserId: owner.userId }, deps) } else { await doPublish(t.streamId, { ref: t.ref }, deps) } } catch (err) { log.warn('push dispatch target failed', { streamId: t.streamId, message: err.message }) } } } module.exports = { fromShardEvent }