Áttekintés
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-út | Mit ad hozzá |
|---|---|
github.com/gp-system/notify | a 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/broadcast | az é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:
- 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. - Sorban, szinkron, a hívó goroutine-jában megpróbálja mindazt a csatornát, amit a
Viamegnevez. - 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). - 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:
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 }
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.