5.7.3 In-app notification fan-in job — a 1.6-pipeline consumer of the SHIPPED 5.1.6 events writing `Notification` rows (actor-excluded, mention-deduped, EXTENSIBLE for 5.4/6.6)
Estimate: 35m · Depends on: 5.7.2
The event CONSUMER — the heart of "fed by a job, never a second emit path". A SECOND consumer (beside the DONE 5.1.6 email job) of the SAME channel-agnostic events, on the same 1.6 defineJob pipeline, that fans each event into Notification rows for the right recipients. It touches NO emit site and adds NO "also notify in-app" call anywhere — the emit already happened (5.1.6); 5.7.3 just subscribes.
Job (lib/jobs/definitions/notificationFanIn.ts, the 1.6 defineJob harness — mirroring mentionNotify.ts, the 5.1.6 in-production reference): consumes the shipped work-item/comment.created + work-item/mentioned events. For each event it computes the recipient set, then writes a Notification row per recipient (the 5.7.2 createMany, one tx) carrying the denormalized data payload the feed row renders (issue key + title, actor, a plain-text excerpt with mention tokens rendered as @Name — reusing the lib/mentions helper 5.1.6 already uses; no raw mention: leakage).
Recipient rules (inherited from the 5.1.6 / 5.4 contract — same semantics, in-app channel): the actor is ALWAYS excluded (never notify yourself — the 5.1.6 rule); mentions are deduped (a user mentioned twice in one comment gets ONE row; a user who is both mentioned and a watcher gets ONE — the 5.4 dedupe rule, mention wins the direct category); view access is re-validated at fan-in time (a recipient who can no longer view the issue gets no row — the 5.1.6 send-time re-check). Idempotent per (eventId × recipientUserId) via the 5.7.2 @@unique + the harness idempotency key — a replay or retry never double-writes; failures land in the DLQ per the 1.6 contract.
EXTENSIBLE — the fan-in seam (no forward dep). The handler dispatches on a small eventType → { category, recipients(event), summary(event) } registry so Story 5.4's work-item/transitioned (the watching category) and Story 6.6's work-item/created + work-item/field.changed fan in by ADDING a registry entry when those events land — with NO change to this job's core and NO dependency on 5.4/6.6 here (those stories document 5.7 as their seam). 5.7.3 ships only the entries for the SHIPPED 5.1.6 events; an unregistered event type is a clean no-op, not an error.
The preference gate is consulted here (the actual gating code is 5.7.6, which this job calls): before writing a recipient's row, check their per-event-type in_app preference; if off, no row. 5.7.3 ships the call site + a permissive default; 5.7.6 lands the NotificationPreference model + the resolver behind it.
Acceptance criteria
- A
work-item/comment.created/work-item/mentionedevent fans into oneNotificationrow per eligible recipient (thecreateManybatch in one tx), with the denormalizeddatapayload the feed renders; the actor is excluded; mentions are deduped (twice-mentioned → one row; mention+watcher → onedirectrow). - View access is re-validated at fan-in (a non-viewer recipient gets no row); idempotency holds (replaying the event / retrying the job never double-writes, per the (eventId × recipient) key); failures land in the DLQ (the 1.6 /
@inngest/testharness). - The handler is registry-driven: adding a new
eventTypeentry is the ONLY change needed for 5.4/6.6 to fan in (asserted by a test that registers a synthetic event type and sees rows); an unregistered event type is a clean no-op. No import of, or dep on, Story 5.4 / 6.6 code. - The in-app preference gate is consulted per recipient via the 5.7.6 resolver (a permissive default until 5.7.6 lands); no emit site is touched and no second notify call is added beside any email send.
pnpm test:coverageholds the gate on the new job code.
Context refs
lib/jobs/definitions/mentionNotify.ts(5.1.6) — the in-production consumer this mirrors (the SECOND consumer of the same events);lib/jobs/defineJob.ts+lib/jobs/types.ts— the harness, idempotency, DLQ + the typedJobEventDataMap(consumed, NOT extended here)- 5.7.2
notificationRepository.createMany+ the@@uniqueidempotency key lib/services/assignableMembersService.ts(6.4) / the 5.1.6 view re-check — the recipient view-validation reusedlib/mentions/— the excerpt/@Name render helper 5.1.6 already uses (no raw token leakage)- Story 5.4 (the
work-item/transitionedwatcher event — thewatchingfan-in slot) + Story 6.6 (work-item/created/work-item/field.changed) — the events that fan in via the registry seam later, with no 5.7 change