notify

Áttekintés

Többcsatornás notificationök: egy payload, több csatorna (database, mail, broadcast), egy közös Hub vezérli.

Egy notification egy recipienthez (egy userhez, nem csak egy e-mail címhez) címzett tartalom, ami több csatornán is kimehet: perzisztálva egy database-inboxba, elküldve mailben, élőben kipusholva a realtime gatewayen keresztül. Hogy melyik csatorna sül el, az teljesen attól függ, mit deklarál maga a notification.

A notify önálló Go modul (github.com/gp-system/notify, +notify/database, +notify/broadcast). A mail-csatornához a mail-ra épül, a database-csatornához a dbx-ra, de szándékosan nem függ a queue-tól: a broadcast fire-and-forget pub/sub, a durable database-csatorna pedig egy sima insert, így itt semmi nem igényel durable job-queue-t. Az aszinkron kézbesítés, ha kell, egy event dispatchálásából és a Hub.Send egy listenerből való hívásából jön, ugyanúgy, mint a mail-nél.

Telepítés

go get github.com/gp-system/notify@v0.1.0 # a kit is ezt a taget használja
go get github.com/gp-system/notify@latest
Import-útMit ad hozzá
github.com/gp-system/notifya Hub-ot, a Notification/Recipient-et, a mail-csatornát
github.com/gp-system/notify/database (+/bunx)a durable database-csatornát, lásd Database
github.com/gp-system/notify/broadcastaz élő push-csatornát és a gateway építőelemeit, lásd Broadcast

A harmadik tudatos absztrakció

A storage és a mail mellett a notify a kit harmadik tudatos kivétele a "centralizálj, ne absztrahálj" elv alól: első-party rendszer, ami a termékek gyártását gyorsítja, nem egy 3rd-party API-t elrejtő wrapper. Vékony marad: csatornánként egy type assertion küldésenként, zéró reflection.

Az alaptípusok

import "github.com/gp-system/notify"

type Notification interface {
    NotificationName() string
    Via(ctx context.Context, r Recipient) []string
}

type Recipient struct {
    ID    string // stabil recipient-azonosító (kit-kiadású JWT-kben egy rbac.Identity.Subject)
    Email string
    Name  string
}

A NotificationName egy stabil, pontozott azonosító ("contact.admin_notification"): a database csatorna ezt perzisztálja, span-ek és logok ezt hordozzák, ugyanaz a konvenció mint az events.Event.EventName-nél. A Via-t küldésenként kiértékeljük, így ugyanaz a notification-típus recipiensenként másképp routolhat, pl. csak database, ha a recipiensnek nincs regisztrált e-mail címe.

A csatornánkénti tartalom egy külön, opcionális interfész, amit a csatorna type-asserttel keres:

type Mailable interface {
    ToMail(ctx context.Context, r Recipient) (*mail.Message, error)
}
type Databasable interface {
    ToDatabase(ctx context.Context, r Recipient) (any, error)
}

Van egy harmadik is, a Broadcastable, ezt a Broadcast oldal tárgyalja, az őt fogyasztó csatornával együtt. Ha a Via egy olyan csatornát nevez meg, amihez a notification nem implementálja a tartalom-interfészt, az a csatorna notify.ErrNoContent-et ad vissza; de megpróbálja (lásd a Hub.Send-et lent), sosem csendben kihagyja.

A Hub

hub := notify.NewHub(
    notify.NewMailChannel(mailer),
    notifydatabase.NewChannel(store),
)
err := hub.Send(ctx, notify.Recipient{ID: "u1", Email: "user@example.com"}, OrderShipped{...})

A NewHub panicol duplikált csatornanévnél: ez boot-idejű drótozási hiba, nem futásidejű állapot. A Send:

  1. Egy delivery ID-t stampel a ctx-be (notify.IDFromContext): a database csatorna ezt használja a sor primary key-jeként, a broadcast csatorna envelope-ja ugyanezt az ID-t hordozza, így egy kliens egy élő push-t egy database-sorral tud egyeztetni refetch nélkül.
  2. Sorban, szinkron, a hívó goroutine-jában megpróbálja mindazt a csatornát, amit a Via megnevez.
  3. Egy nem regisztrált csatornanevet csendben kihagy (discard-szemantika, ugyanaz a "nincs konfigurálva" történet, ami az smtp.NewIfConfigured-nál is megvan).
  4. Minden megnevezett csatornát megpróbál egy korábbi hiba ellenére is; a hibák össze vannak fűzve (errors.Join), mindegyik a csatorna és a notification nevével becsomagolva.

Nincs beépített queued mód: a notify ugyanazt a szabályt követi, mint a mail. Aszinkron kézbesítéshez dispatchálj egy eventet és hívd a Hub.Send-et egy listenerből, a meglévő at-least-once retry és idempotencia garanciákkal. Egy listener-redelivery duplikálhat egy database-notificationt; ez egy elfogadott, dokumentált mellékhatás, ugyanaz, amit az eventek is hordoznak.

A mail-csatorna

notify.NewMailChannel(mailer) // mailer: bármilyen mail.Mailer (SMTP, discard, memory)

Ugyanarra a kit mail.Mailer-re delegál, amit az add mail drótoz be: egy projekt mail-transzportja közös a direkt mail.Mailer-küldések és a notificationök között. Ha a ToMail visszaadott *mail.Message-én nincs To beállítva, a csatorna automatikusan r.Email/r.Name-re routolja.

Az add mail és az add notification független és összeadódik: a add mail-t olyan tartalomhoz használd, ami közvetlenül egy címre megy, recipient-fogalom nélkül (egy contact-form visszaigazolás); az add notification-t olyan tartalomhoz, ami egy recipiensnek szól, akár több csatornán. Egy notification ToMail-je ugyanazt a mail.Message/mail.Template buildert használja, mint egy mail-notifier Notify-ja.

A database-csatornáért (notify/database) lásd a Database oldalt; a broadcast-csatornáért (notify/broadcast) lásd a Broadcast oldalt.

add notification

gpsystem add notification <module> <name>

Generál egy internal/modules/<module>/notifications/<name>.go-t: egy üres payload structot, Via-t (alapból [database, mail]), egy ToMail-t a kit mail.Message/mail.Template-jére építve, egy ToDatabase-t ami önmagát adja vissza, plusz a templates/<name>.{html,txt}.tmpl fájlokat. A projekt első notificationjénél bedrótoz egy közös notify.Sender-t (mail + database csatorna, plusz broadcast, ha a realtime gateway jelen van) minden belépési pontba, és hozzáadja a notifications tábla migrációt. Lásd az add notification referenciát a pontos generált formáért és a wiring-anchorokért.

Tesztelés

mem := notify.NewMemory(notify.ChannelDatabase) // Deliveries()-t rögzít bármely csatornanévre
hub := notify.NewHub(mem)

Tartalom-renderelési lefedettséghez a valós csatornákat érdemes valós driverek felett használni: notify.NewMailChannel(mail.NewMemory()) és notifydatabase.NewChannel(notifydatabase.NewMemoryStore()): a MemoryStore a teljes Store-t implementálja (az olvasó oldalt is), így egy termék Postgres nélkül tesztelheti az inbox-handlereit. A csatornánkénti test double-ökért lásd a Database#memorystore és a Broadcast#memory szakaszt.

Kidolgozott példa: a contact-email flow

A mail oldal kidolgozott példája egy contact-form beküldés admin-értesítését és a beküldő visszaigazolását két direkt mailként küldi. Notificationként újragondolva az admin-oldal egy notify.Notification lesz, az admin fiókhoz címezve (subject "admin" egy single-admin projektben), kézbesítve mind a database csatornán (perzisztált admin-inbox), mind a mail csatornán (ugyanarra a címre, mint a direkt küldés); míg a beküldő visszaigazolása, aminek nincs recipient-fogalma a form e-mail címén túl, marad egy direkt mail.Mailer-küldés:

internal/modules/contact/notifications/adminnotification.go
package notifications

type AdminNotification struct {
    contactmail.ContactNotification // ugyanaz a payload, amit a beküldőmail renderel
}

func (AdminNotification) NotificationName() string { return "contact.admin_notification" }

func (AdminNotification) Via(context.Context, notify.Recipient) []string {
    return []string{notify.ChannelDatabase, notify.ChannelMail}
}

func (n AdminNotification) ToMail(context.Context, notify.Recipient) (*kitmail.Message, error) {
    return kitmail.NewMessage().
        WithSubject(adminSubjectLine(n.ContactNotification)).
        WithBody(kitmail.Template(templatesFS, "templates/adminnotification.html.tmpl", n).
            WithTextTemplate("templates/adminnotification.txt.tmpl")), nil
}

func (n AdminNotification) ToDatabase(context.Context, notify.Recipient) (any, error) { return n, nil }
internal/modules/contact/listeners/sendcontactemail.go
func sendContactEmail(deps contact.Dependencies) func(context.Context, contactevents.ContactSubmitted) error {
    requesterNotifier := contactmail.New(deps.Mailer)
    return func(ctx context.Context, ev contactevents.ContactSubmitted) error {
        payload := contactmail.ContactNotification{Name: ev.Name, Email: ev.Email /* ... */}

        adminErr := deps.Notifier.Send(ctx,
            notify.Recipient{ID: "admin", Email: deps.Mail.AdminEmail},
            contactnotifications.AdminNotification{ContactNotification: payload},
        )
        requesterErr := requesterNotifier.Notify(ctx, payload)
        return errors.Join(adminErr, requesterErr)
    }
}

Az AdminNotification ugyanazt a ContactNotification payloadot embeddeli, amit a beküldőmail renderel, így mindkét felület egy közös igazságforrásból szinkronban marad. Az admin pedig ingyen kap egy perzisztált, lekérdezhető notification-történetet, a beküldő-oldali rész változtatása nélkül.

Kapcsolódó oldalak

  • Database: a durable, lekérdezhető inbox-csatorna.
  • Broadcast: az élő push-csatorna és a Valkey-bekötése.
  • Realtime: a gateway, ami a kliens-kapcsolatokat tartja a broadcast-csatornához.
  • Mail: a Mailer, amire ennek a modulnak a mail-csatornája delegál.
Copyright © 2026