[{"data":1,"prerenderedAt":2263},["ShallowReactive",2],{"navigation_docs_en":3,"-en-events-overview":441,"-en-events-overview-surround":2258},[4,45,60,81,112,129,146,173,197,213,242,279,356,372,384,403,429],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":44},"Getting Started","i-lucide-rocket","\u002Fen\u002Fgetting-started","en\u002F1.getting-started",[10,15,20,24,29,34,39],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fen\u002Fgetting-started\u002Fintroduction","en\u002F1.getting-started\u002F1.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Coming from Laravel","\u002Fen\u002Fgetting-started\u002Fcoming-from-laravel","en\u002F1.getting-started\u002F2.coming-from-laravel","i-lucide-arrow-right-left",{"title":21,"path":22,"stem":23,"icon":19},"Coming from Symfony","\u002Fen\u002Fgetting-started\u002Fcoming-from-symfony","en\u002F1.getting-started\u002F3.coming-from-symfony",{"title":25,"path":26,"stem":27,"icon":28},"Installation","\u002Fen\u002Fgetting-started\u002Finstallation","en\u002F1.getting-started\u002F4.installation","i-lucide-download",{"title":30,"path":31,"stem":32,"icon":33},"Your first module","\u002Fen\u002Fgetting-started\u002Ffirst-module","en\u002F1.getting-started\u002F5.first-module","i-lucide-package-plus",{"title":35,"path":36,"stem":37,"icon":38},"Project structure","\u002Fen\u002Fgetting-started\u002Fproject-structure","en\u002F1.getting-started\u002F6.project-structure","i-lucide-folder-tree",{"title":40,"path":41,"stem":42,"icon":43},"The shop sample application","\u002Fen\u002Fgetting-started\u002Fsample-app","en\u002F1.getting-started\u002F7.sample-app","i-lucide-shopping-cart",false,{"title":46,"icon":47,"path":48,"stem":49,"children":50,"page":44},"queue","i-lucide-layers","\u002Fen\u002Fqueue","en\u002F10.queue",[51,56],{"title":52,"path":53,"stem":54,"icon":55},"Overview","\u002Fen\u002Fqueue\u002Foverview","en\u002F10.queue\u002F1.overview","i-lucide-list-ordered",{"title":57,"path":58,"stem":59,"icon":47},"Tasks","\u002Fen\u002Fqueue\u002Ftasks","en\u002F10.queue\u002F2.tasks",{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":44},"events","i-lucide-workflow","\u002Fen\u002Fevents","en\u002F11.events",[66,71,76],{"title":67,"path":68,"stem":69,"icon":70},"Events","\u002Fen\u002Fevents\u002Foverview","en\u002F11.events\u002F1.overview","i-lucide-radio",{"title":72,"path":73,"stem":74,"icon":75},"Outbox","\u002Fen\u002Fevents\u002Foutbox","en\u002F11.events\u002F2.outbox","i-lucide-inbox",{"title":77,"path":78,"stem":79,"icon":80},"Scheduling","\u002Fen\u002Fevents\u002Fscheduler","en\u002F11.events\u002F3.scheduler","i-lucide-calendar-clock",{"title":82,"icon":83,"path":84,"stem":85,"children":86,"page":44},"auth","i-lucide-shield-check","\u002Fen\u002Fauth","en\u002F12.auth",[87,92,97,102,107],{"title":88,"path":89,"stem":90,"icon":91},"Authentication","\u002Fen\u002Fauth\u002Foverview","en\u002F12.auth\u002F1.overview","i-lucide-key-round",{"title":93,"path":94,"stem":95,"icon":96},"RBAC","\u002Fen\u002Fauth\u002Frbac","en\u002F12.auth\u002F2.rbac","i-lucide-users",{"title":98,"path":99,"stem":100,"icon":101},"Policies","\u002Fen\u002Fauth\u002Fpolicy","en\u002F12.auth\u002F3.policy","i-lucide-gavel",{"title":103,"path":104,"stem":105,"icon":106},"Using auth standalone","\u002Fen\u002Fauth\u002Fstandalone","en\u002F12.auth\u002F4.standalone","i-lucide-plug",{"title":108,"path":109,"stem":110,"icon":111},"Social login with goth, standalone","\u002Fen\u002Fauth\u002Fsocial-standalone","en\u002F12.auth\u002F5.social-standalone","i-lucide-log-in",{"title":113,"icon":114,"path":115,"stem":116,"children":117,"page":44},"mail","i-lucide-mail","\u002Fen\u002Fmail","en\u002F13.mail",[118,121,125],{"title":52,"path":119,"stem":120,"icon":114},"\u002Fen\u002Fmail\u002Foverview","en\u002F13.mail\u002F1.overview",{"title":122,"path":123,"stem":124,"icon":114},"SMTP","\u002Fen\u002Fmail\u002Fsmtp","en\u002F13.mail\u002F2.smtp",{"title":126,"path":127,"stem":128,"icon":114},"MJML","\u002Fen\u002Fmail\u002Fmjml","en\u002F13.mail\u002F3.mjml",{"title":130,"icon":131,"path":132,"stem":133,"children":134,"page":44},"notify","i-lucide-bell","\u002Fen\u002Fnotify","en\u002F14.notify",[135,138,142],{"title":52,"path":136,"stem":137,"icon":131},"\u002Fen\u002Fnotify\u002Foverview","en\u002F14.notify\u002F1.overview",{"title":139,"path":140,"stem":141,"icon":131},"Database channel","\u002Fen\u002Fnotify\u002Fdatabase","en\u002F14.notify\u002F2.database",{"title":143,"path":144,"stem":145,"icon":131},"Broadcast channel","\u002Fen\u002Fnotify\u002Fbroadcast","en\u002F14.notify\u002F3.broadcast",{"title":147,"icon":148,"path":149,"stem":150,"children":151,"page":44},"storage","i-lucide-hard-drive","\u002Fen\u002Fstorage","en\u002F15.storage",[152,155,159,163,168],{"title":52,"path":153,"stem":154,"icon":148},"\u002Fen\u002Fstorage\u002Foverview","en\u002F15.storage\u002F1.overview",{"title":156,"path":157,"stem":158},"Disk API","\u002Fen\u002Fstorage\u002Fdisk-api","en\u002F15.storage\u002F2.disk-api",{"title":160,"path":161,"stem":162,"icon":148},"Memory and local disk","\u002Fen\u002Fstorage\u002Flocal","en\u002F15.storage\u002F3.local",{"title":164,"path":165,"stem":166,"icon":167},"S3","\u002Fen\u002Fstorage\u002Fs3","en\u002F15.storage\u002F4.s3","i-lucide-cloud",{"title":169,"path":170,"stem":171,"icon":172},"Testing","\u002Fen\u002Fstorage\u002Ftesting","en\u002F15.storage\u002F5.testing","i-lucide-flask-conical",{"title":174,"icon":175,"path":176,"stem":177,"children":178,"page":44},"telemetry","i-lucide-activity","\u002Fen\u002Ftelemetry","en\u002F16.telemetry",[179,182,187,192],{"title":52,"path":180,"stem":181,"icon":175},"\u002Fen\u002Ftelemetry\u002Foverview","en\u002F16.telemetry\u002F1.overview",{"title":183,"path":184,"stem":185,"icon":186},"Logging","\u002Fen\u002Ftelemetry\u002Flogging","en\u002F16.telemetry\u002F2.logging","i-lucide-scroll-text",{"title":188,"path":189,"stem":190,"icon":191},"OpenTelemetry","\u002Fen\u002Ftelemetry\u002Fopentelemetry","en\u002F16.telemetry\u002F3.opentelemetry","i-lucide-radar",{"title":193,"path":194,"stem":195,"icon":196},"Sentry","\u002Fen\u002Ftelemetry\u002Fsentry","en\u002F16.telemetry\u002F4.sentry","i-lucide-bug",{"title":198,"icon":199,"path":200,"stem":201,"children":202,"page":44},"Reference","i-lucide-book-open","\u002Fen\u002Freference","en\u002F17.reference",[203,208],{"title":204,"path":205,"stem":206,"icon":207},"Configuration","\u002Fen\u002Freference\u002Fconfiguration","en\u002F17.reference\u002F1.configuration","i-lucide-settings",{"title":209,"path":210,"stem":211,"icon":212},"External dependencies","\u002Fen\u002Freference\u002Fdependencies","en\u002F17.reference\u002F2.dependencies","i-lucide-package",{"title":214,"icon":215,"path":216,"stem":217,"children":218,"page":44},"Concepts","i-lucide-lightbulb","\u002Fen\u002Fconcepts","en\u002F2.concepts",[219,223,228,232,237],{"title":220,"path":221,"stem":222,"icon":47},"Architecture","\u002Fen\u002Fconcepts\u002Farchitecture","en\u002F2.concepts\u002F1.architecture",{"title":224,"path":225,"stem":226,"icon":227},"Error model","\u002Fen\u002Fconcepts\u002Ferror-model","en\u002F2.concepts\u002F2.error-model","i-lucide-shield-alert",{"title":204,"path":229,"stem":230,"icon":231},"\u002Fen\u002Fconcepts\u002Fconfiguration","en\u002F2.concepts\u002F3.configuration","i-lucide-settings-2",{"title":233,"path":234,"stem":235,"icon":236},"Codegen pipeline","\u002Fen\u002Fconcepts\u002Fcodegen-pipeline","en\u002F2.concepts\u002F4.codegen-pipeline","i-lucide-file-json",{"title":238,"path":239,"stem":240,"icon":241},"Design patterns","\u002Fen\u002Fconcepts\u002Fdesign-patterns","en\u002F2.concepts\u002F5.design-patterns","i-lucide-puzzle",{"title":243,"icon":244,"path":245,"stem":246,"children":247,"page":44},"Kit","i-lucide-box","\u002Fen\u002Fkit","en\u002F3.kit",[248,251,256,261,266,270,275],{"title":52,"path":249,"stem":250,"icon":244},"\u002Fen\u002Fkit\u002Foverview","en\u002F3.kit\u002F1.overview",{"title":252,"path":253,"stem":254,"icon":255},"Application lifecycle","\u002Fen\u002Fkit\u002Fapp","en\u002F3.kit\u002F2.app","i-lucide-power",{"title":257,"path":258,"stem":259,"icon":260},"Server","\u002Fen\u002Fkit\u002Fserver","en\u002F3.kit\u002F3.server","i-lucide-server",{"title":262,"path":263,"stem":264,"icon":265},"Worker","\u002Fen\u002Fkit\u002Fworker","en\u002F3.kit\u002F4.worker","i-lucide-cog",{"title":267,"path":268,"stem":269,"icon":70},"Realtime","\u002Fen\u002Fkit\u002Frealtime","en\u002F3.kit\u002F5.realtime",{"title":271,"path":272,"stem":273,"icon":274},"Migrations","\u002Fen\u002Fkit\u002Fmigrations","en\u002F3.kit\u002F6.migrations","i-lucide-file-stack",{"title":276,"path":277,"stem":278,"icon":111},"Social login","\u002Fen\u002Fkit\u002Fsocial-login","en\u002F3.kit\u002F7.social-login",{"title":280,"icon":281,"path":282,"stem":283,"children":284,"page":44},"CLI Reference","i-lucide-terminal","\u002Fen\u002Fcli","en\u002F4.cli",[285,288,293,298,303,308,313,318,323,328,333,337,342,347,351],{"title":52,"path":286,"stem":287,"icon":281},"\u002Fen\u002Fcli\u002Foverview","en\u002F4.cli\u002F1.overview",{"title":289,"path":290,"stem":291,"icon":292},"add db","\u002Fen\u002Fcli\u002Fadd-db","en\u002F4.cli\u002F10.add-db","i-lucide-database-zap",{"title":294,"path":295,"stem":296,"icon":297},"add compose \u002F add docker","\u002Fen\u002Fcli\u002Fadd-compose","en\u002F4.cli\u002F11.add-compose","i-lucide-container",{"title":299,"path":300,"stem":301,"icon":302},"add mail","\u002Fen\u002Fcli\u002Fadd-mail","en\u002F4.cli\u002F12.add-mail","i-lucide-mail-plus",{"title":304,"path":305,"stem":306,"icon":307},"add notification","\u002Fen\u002Fcli\u002Fadd-notification","en\u002F4.cli\u002F13.add-notification","i-lucide-bell-plus",{"title":309,"path":310,"stem":311,"icon":312},"add realtime","\u002Fen\u002Fcli\u002Fadd-realtime","en\u002F4.cli\u002F14.add-realtime","i-lucide-radio-tower",{"title":314,"path":315,"stem":316,"icon":317},"add seeder","\u002Fen\u002Fcli\u002Fadd-seeder","en\u002F4.cli\u002F15.add-seeder","i-lucide-sprout",{"title":319,"path":320,"stem":321,"icon":322},"new project","\u002Fen\u002Fcli\u002Fnew-project","en\u002F4.cli\u002F2.new-project","i-lucide-folder-plus",{"title":324,"path":325,"stem":326,"icon":327},"new module","\u002Fen\u002Fcli\u002Fnew-module","en\u002F4.cli\u002F3.new-module","i-lucide-blocks",{"title":329,"path":330,"stem":331,"icon":332},"add surface","\u002Fen\u002Fcli\u002Fadd-surface","en\u002F4.cli\u002F4.add-surface","i-lucide-layers-2",{"title":334,"path":335,"stem":336,"icon":244},"add core","\u002Fen\u002Fcli\u002Fadd-core","en\u002F4.cli\u002F5.add-core",{"title":338,"path":339,"stem":340,"icon":341},"add handler","\u002Fen\u002Fcli\u002Fadd-handler","en\u002F4.cli\u002F6.add-handler","i-lucide-webhook",{"title":343,"path":344,"stem":345,"icon":346},"new migration","\u002Fen\u002Fcli\u002Fnew-migration","en\u002F4.cli\u002F7.new-migration","i-lucide-file-plus",{"title":348,"path":349,"stem":350,"icon":265},"worker generators","\u002Fen\u002Fcli\u002Fworker-generators","en\u002F4.cli\u002F8.worker-generators",{"title":352,"path":353,"stem":354,"icon":355},"upgrade templates","\u002Fen\u002Fcli\u002Fupgrade-templates","en\u002F4.cli\u002F9.upgrade-templates","i-lucide-refresh-cw",{"title":357,"icon":227,"path":358,"stem":359,"children":360,"page":44},"errs","\u002Fen\u002Ferrs","en\u002F5.errs",[361,364,368],{"title":52,"path":362,"stem":363,"icon":227},"\u002Fen\u002Ferrs\u002Foverview","en\u002F5.errs\u002F1.overview",{"title":365,"path":366,"stem":367},"API","\u002Fen\u002Ferrs\u002Fapi","en\u002F5.errs\u002F2.api",{"title":369,"path":370,"stem":371,"icon":227},"Integration","\u002Fen\u002Ferrs\u002Fintegration","en\u002F5.errs\u002F3.integration",{"title":373,"icon":231,"path":374,"stem":375,"children":376,"page":44},"envconf","\u002Fen\u002Fenvconf","en\u002F6.envconf",[377,380],{"title":52,"path":378,"stem":379,"icon":231},"\u002Fen\u002Fenvconf\u002Foverview","en\u002F6.envconf\u002F1.overview",{"title":381,"path":382,"stem":383},"Recipes","\u002Fen\u002Fenvconf\u002Frecipes","en\u002F6.envconf\u002F2.recipes",{"title":385,"icon":386,"path":387,"stem":388,"children":389,"page":44},"httperr","i-lucide-globe","\u002Fen\u002Fhttperr","en\u002F7.httperr",[390,393,398],{"title":52,"path":391,"stem":392,"icon":386},"\u002Fen\u002Fhttperr\u002Foverview","en\u002F7.httperr\u002F1.overview",{"title":394,"path":395,"stem":396,"icon":397},"Error responses","\u002Fen\u002Fhttperr\u002Ferror-responses","en\u002F7.httperr\u002F2.error-responses","i-lucide-octagon-alert",{"title":399,"path":400,"stem":401,"icon":402},"Validation","\u002Fen\u002Fhttperr\u002Fvalidation","en\u002F7.httperr\u002F3.validation","i-lucide-badge-check",{"title":404,"icon":405,"path":406,"stem":407,"children":408,"page":44},"dbx","i-lucide-database","\u002Fen\u002Fdbx","en\u002F8.dbx",[409,412,416,420,425],{"title":52,"path":410,"stem":411,"icon":405},"\u002Fen\u002Fdbx\u002Foverview","en\u002F8.dbx\u002F1.overview",{"title":413,"path":414,"stem":415,"icon":106},"pgx","\u002Fen\u002Fdbx\u002Fpg","en\u002F8.dbx\u002F2.pg",{"title":417,"path":418,"stem":419,"icon":47},"bun","\u002Fen\u002Fdbx\u002Fbunx","en\u002F8.dbx\u002F3.bunx",{"title":421,"path":422,"stem":423,"icon":424},"Transactions","\u002Fen\u002Fdbx\u002Ftransactions","en\u002F8.dbx\u002F4.transactions","i-lucide-git-merge",{"title":426,"path":427,"stem":428,"icon":317},"Seeders","\u002Fen\u002Fdbx\u002Fseed","en\u002F8.dbx\u002F5.seed",{"title":430,"icon":55,"path":431,"stem":432,"children":433,"page":44},"paginate","\u002Fen\u002Fpaginate","en\u002F9.paginate",[434,437],{"title":52,"path":435,"stem":436,"icon":55},"\u002Fen\u002Fpaginate\u002Foverview","en\u002F9.paginate\u002F1.overview",{"title":438,"path":439,"stem":440,"icon":19},"Cursor pagination","\u002Fen\u002Fpaginate\u002Fcursor","en\u002F9.paginate\u002F2.cursor",{"id":442,"title":67,"body":443,"description":2251,"extension":2252,"links":2253,"meta":2254,"navigation":2255,"path":68,"seo":2256,"stem":69,"__hash__":2257},"docs_en\u002Fen\u002F11.events\u002F1.overview.md",{"type":444,"value":445,"toc":2239},"minimark",[446,457,508,532,548,552,579,633,637,652,774,794,808,812,835,1182,1189,1274,1280,1284,1301,1307,1364,1368,1382,1548,1551,1865,1878,1881,1938,1974,1978,2005,2008,2012,2023,2098,2101,2132,2136,2171,2175,2217,2221,2235],[447,448,449,450,453,454,456],"p",{},"Add ",[451,452,61],"code",{}," alongside ",[451,455,46],{}," to dispatch and listen for events from your module:",[458,459,464],"pre",{"className":460,"code":461,"language":462,"meta":463,"style":463},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import (\n    \"github.com\u002Fgp-system\u002Fevents\"\n    \"github.com\u002Fgp-system\u002Fqueue\"\n)\n","go","",[451,465,466,479,492,502],{"__ignoreMap":463},[467,468,471,475],"span",{"class":469,"line":470},"line",1,[467,472,474],{"class":473},"s7zQu","import",[467,476,478],{"class":477},"sMK4o"," (\n",[467,480,482,485,489],{"class":469,"line":481},2,[467,483,484],{"class":477},"    \"",[467,486,488],{"class":487},"sBMFI","github.com\u002Fgp-system\u002Fevents",[467,490,491],{"class":477},"\"\n",[467,493,495,497,500],{"class":469,"line":494},3,[467,496,484],{"class":477},[467,498,499],{"class":487},"github.com\u002Fgp-system\u002Fqueue",[467,501,491],{"class":477},[467,503,505],{"class":469,"line":504},4,[467,506,507],{"class":477},")\n",[447,509,510,512,513,518,519,523,524,527,528,531],{},[451,511,61],{}," is a standalone events-and-listeners module built on ",[514,515,516],"a",{"href":53},[451,517,46],{},": every listener is queued from the start, running asynchronously by default. Application code ",[520,521,522],"strong",{},"dispatches"," an event, listeners subscribe by type, and every listener runs as an ",[520,525,526],{},"independent asynq task"," with its own retry budget: one event fans out to N listener tasks, and a failing listener's retry never re-runs the others. It works in any Go program, with or without the kit: a plain ",[451,529,530],{},"net\u002Fhttp"," service, a CLI tool, or a Lambda can dispatch and listen for events with nothing but this module and Valkey.",[447,533,534,535,538,539,542,543,547],{},"Delivery is ",[520,536,537],{},"at-least-once"," end to end (outbox → relay → asynq → listener), so two rules hold the whole thing together: in state-changing flows dispatch through the ",[514,540,541],{"href":73},"outbox",", and write your listeners to be ",[514,544,546],{"href":545},"#idempotency","idempotent",".",[549,550,25],"h2",{"id":551},"installation",[458,553,557],{"className":554,"code":555,"language":556,"meta":463,"style":463},"language-sh shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","go get github.com\u002Fgp-system\u002Fevents@v0.1.0   # the kit uses this same tag\n# or: go get github.com\u002Fgp-system\u002Fevents@latest\n","sh",[451,558,559,574],{"__ignoreMap":463},[467,560,561,563,567,570],{"class":469,"line":470},[467,562,462],{"class":487},[467,564,566],{"class":565},"sfazB"," get",[467,568,569],{"class":565}," github.com\u002Fgp-system\u002Fevents@v0.1.0",[467,571,573],{"class":572},"sHwdD","   # the kit uses this same tag\n",[467,575,576],{"class":469,"line":481},[467,577,578],{"class":572},"# or: go get github.com\u002Fgp-system\u002Fevents@latest\n",[447,580,581,583,584,589,590,595,596,601,602,605,606,609,610,613,614,619,620,605,623,589,626,629,630,632],{},[451,582,61],{}," pulls in ",[514,585,586],{"href":362},[451,587,588],{},"gp-system\u002Ferrs"," and ",[514,591,592],{"href":53},[451,593,594],{},"gp-system\u002Fqueue"," directly; the transactional ",[514,597,598],{"href":73},[451,599,600],{},"events\u002Foutbox"," subpackage additionally needs ",[451,603,604],{},"gp-system\u002Fdbx",", ",[451,607,608],{},"jackc\u002Fpgx\u002Fv5"," and, if you use the bun-flavored store, ",[451,611,612],{},"uptrace\u002Fbun","; the ",[514,615,616],{"href":78},[451,617,618],{},"events\u002Fscheduler"," subpackage needs nothing beyond the root module. ",[451,621,622],{},"hibiken\u002Fasynq",[451,624,625],{},"redis\u002Fgo-redis\u002Fv9",[451,627,628],{},"google\u002Fuuid"," come along transitively through ",[451,631,46],{},". None of this requires the kit: import the subpackage you need and nothing else.",[549,634,636],{"id":635},"defining-an-event","Defining an event",[447,638,639,640,643,644,647,648,651],{},"An event is a plain struct with a stable ",[451,641,642],{},"EventName()",". The shop's ",[451,645,646],{},"orderPlaced"," event was generated by ",[451,649,650],{},"add event shop orderPlaced","; you add the payload fields:",[458,653,656],{"className":460,"code":654,"filename":655,"language":462,"meta":463,"style":463},"package events\n\ntype OrderPlaced struct {\n    OrderID string `json:\"orderId\"`\n    Email   string `json:\"email\"`\n}\n\nfunc (OrderPlaced) EventName() string { return \"shop.orderPlaced\" }\n","internal\u002Fmodules\u002Fshop\u002Fevents\u002Forder_placed.go",[451,657,658,666,672,686,705,720,726,731],{"__ignoreMap":463},[467,659,660,663],{"class":469,"line":470},[467,661,662],{"class":477},"package",[467,664,665],{"class":487}," events\n",[467,667,668],{"class":469,"line":481},[467,669,671],{"emptyLinePlaceholder":670},true,"\n",[467,673,674,677,680,683],{"class":469,"line":494},[467,675,676],{"class":477},"type",[467,678,679],{"class":487}," OrderPlaced",[467,681,682],{"class":477}," struct",[467,684,685],{"class":477}," {\n",[467,687,688,692,696,699,702],{"class":469,"line":504},[467,689,691],{"class":690},"sTEyZ","    OrderID ",[467,693,695],{"class":694},"spNyl","string",[467,697,698],{"class":477}," `",[467,700,701],{"class":565},"json:\"orderId\"",[467,703,704],{"class":477},"`\n",[467,706,708,711,713,715,718],{"class":469,"line":707},5,[467,709,710],{"class":690},"    Email   ",[467,712,695],{"class":694},[467,714,698],{"class":477},[467,716,717],{"class":565},"json:\"email\"",[467,719,704],{"class":477},[467,721,723],{"class":469,"line":722},6,[467,724,725],{"class":477},"}\n",[467,727,729],{"class":469,"line":728},7,[467,730,671],{"emptyLinePlaceholder":670},[467,732,734,737,740,743,746,750,753,756,759,762,765,768,771],{"class":469,"line":733},8,[467,735,736],{"class":477},"func",[467,738,739],{"class":477}," (",[467,741,742],{"class":487},"OrderPlaced",[467,744,745],{"class":477},")",[467,747,749],{"class":748},"s2Zo4"," EventName",[467,751,752],{"class":477},"()",[467,754,755],{"class":694}," string",[467,757,758],{"class":477}," {",[467,760,761],{"class":473}," return",[467,763,764],{"class":477}," \"",[467,766,767],{"class":565},"shop.orderPlaced",[467,769,770],{"class":477},"\"",[467,772,773],{"class":477}," }\n",[447,775,776,777,780,781,784,785,789,790,793],{},"The generator's naming convention is ",[451,778,779],{},"\u003Cmodule>.\u003CeventName>"," in camelCase, and it also becomes the ",[451,782,783],{},"event:shop.orderPlaced"," ",[514,786,788],{"href":787},"\u002Fen\u002Fqueue\u002Ftasks#task-types","task type",". The name is ",[520,791,792],{},"persisted"," (in the outbox table and in Valkey), so keep it stable across deploys; if you rename it, tasks already in flight find no handler under the new deploy.",[795,796,797,799,800,803,804,807],"warning",{},[451,798,642],{}," must work on the type's ",[520,801,802],{},"zero value",": register a value type, not a pointer. ",[451,805,806],{},"Listen"," reads the name from the zero value and panics immediately at startup when it is empty or the call blows up, so a broken registration never reaches production.",[549,809,811],{"id":810},"dispatch","Dispatch",[447,813,814,815,818,819,822,823,826,827,830,831,834],{},"The ",[451,816,817],{},"Dispatcher"," is injected into the module's ",[451,820,821],{},"Dependencies"," (the first ",[451,824,825],{},"add event"," wires it into both entry points). The shop's ordering rule (in the module's ",[451,828,829],{},"core\u002F"," package) dispatches ",[520,832,833],{},"inside"," the transaction:",[458,836,839],{"className":460,"code":837,"filename":838,"language":462,"meta":463,"style":463},"func (s *Service) PlaceOrder(ctx context.Context, in PlaceOrderInput) (Order, error) {\n    var order Order\n    err := s.tx.WithinTransaction(ctx, func(ctx context.Context) error {\n        var err error\n        if order, err = s.orders.Insert(ctx, in); err != nil {\n            return err\n        }\n        if err = s.stock.Decrement(ctx, in.Items); err != nil {\n            return err\n        }\n        return s.dispatcher.Dispatch(ctx, events.OrderPlaced{\n            OrderID: order.ID,\n            Email:   order.Email,\n        })\n    })\n    return order, err\n}\n","internal\u002Fmodules\u002Fshop\u002Fcore\u002Fcore.go",[451,840,841,901,912,956,967,1015,1023,1028,1071,1078,1083,1116,1135,1153,1159,1165,1177],{"__ignoreMap":463},[467,842,843,845,847,851,854,857,859,862,865,868,871,873,876,879,882,885,887,889,892,894,897,899],{"class":469,"line":470},[467,844,736],{"class":477},[467,846,739],{"class":477},[467,848,850],{"class":849},"sHdIc","s ",[467,852,853],{"class":477},"*",[467,855,856],{"class":487},"Service",[467,858,745],{"class":477},[467,860,861],{"class":748}," PlaceOrder",[467,863,864],{"class":477},"(",[467,866,867],{"class":849},"ctx",[467,869,870],{"class":487}," context",[467,872,547],{"class":477},[467,874,875],{"class":487},"Context",[467,877,878],{"class":477},",",[467,880,881],{"class":849}," in",[467,883,884],{"class":487}," PlaceOrderInput",[467,886,745],{"class":477},[467,888,739],{"class":477},[467,890,891],{"class":487},"Order",[467,893,878],{"class":477},[467,895,896],{"class":694}," error",[467,898,745],{"class":477},[467,900,685],{"class":477},[467,902,903,906,909],{"class":469,"line":481},[467,904,905],{"class":477},"    var",[467,907,908],{"class":690}," order ",[467,910,911],{"class":487},"Order\n",[467,913,914,917,920,923,925,928,930,933,935,937,939,942,944,946,948,950,952,954],{"class":469,"line":494},[467,915,916],{"class":690},"    err ",[467,918,919],{"class":477},":=",[467,921,922],{"class":690}," s",[467,924,547],{"class":477},[467,926,927],{"class":690},"tx",[467,929,547],{"class":477},[467,931,932],{"class":748},"WithinTransaction",[467,934,864],{"class":477},[467,936,867],{"class":690},[467,938,878],{"class":477},[467,940,941],{"class":477}," func(",[467,943,867],{"class":849},[467,945,870],{"class":487},[467,947,547],{"class":477},[467,949,875],{"class":487},[467,951,745],{"class":477},[467,953,896],{"class":694},[467,955,685],{"class":477},[467,957,958,961,964],{"class":469,"line":504},[467,959,960],{"class":477},"        var",[467,962,963],{"class":690}," err ",[467,965,966],{"class":694},"error\n",[467,968,969,972,975,977,979,982,984,986,989,991,994,996,998,1000,1002,1005,1007,1010,1013],{"class":469,"line":707},[467,970,971],{"class":473},"        if",[467,973,974],{"class":690}," order",[467,976,878],{"class":477},[467,978,963],{"class":690},[467,980,981],{"class":477},"=",[467,983,922],{"class":690},[467,985,547],{"class":477},[467,987,988],{"class":690},"orders",[467,990,547],{"class":477},[467,992,993],{"class":748},"Insert",[467,995,864],{"class":477},[467,997,867],{"class":690},[467,999,878],{"class":477},[467,1001,881],{"class":690},[467,1003,1004],{"class":477},");",[467,1006,963],{"class":690},[467,1008,1009],{"class":477},"!=",[467,1011,1012],{"class":477}," nil",[467,1014,685],{"class":477},[467,1016,1017,1020],{"class":469,"line":722},[467,1018,1019],{"class":473},"            return",[467,1021,1022],{"class":690}," err\n",[467,1024,1025],{"class":469,"line":728},[467,1026,1027],{"class":477},"        }\n",[467,1029,1030,1032,1034,1036,1038,1040,1043,1045,1048,1050,1052,1054,1056,1058,1061,1063,1065,1067,1069],{"class":469,"line":733},[467,1031,971],{"class":473},[467,1033,963],{"class":690},[467,1035,981],{"class":477},[467,1037,922],{"class":690},[467,1039,547],{"class":477},[467,1041,1042],{"class":690},"stock",[467,1044,547],{"class":477},[467,1046,1047],{"class":748},"Decrement",[467,1049,864],{"class":477},[467,1051,867],{"class":690},[467,1053,878],{"class":477},[467,1055,881],{"class":690},[467,1057,547],{"class":477},[467,1059,1060],{"class":690},"Items",[467,1062,1004],{"class":477},[467,1064,963],{"class":690},[467,1066,1009],{"class":477},[467,1068,1012],{"class":477},[467,1070,685],{"class":477},[467,1072,1074,1076],{"class":469,"line":1073},9,[467,1075,1019],{"class":473},[467,1077,1022],{"class":690},[467,1079,1081],{"class":469,"line":1080},10,[467,1082,1027],{"class":477},[467,1084,1086,1089,1091,1093,1096,1098,1100,1102,1104,1106,1109,1111,1113],{"class":469,"line":1085},11,[467,1087,1088],{"class":473},"        return",[467,1090,922],{"class":690},[467,1092,547],{"class":477},[467,1094,1095],{"class":690},"dispatcher",[467,1097,547],{"class":477},[467,1099,811],{"class":748},[467,1101,864],{"class":477},[467,1103,867],{"class":690},[467,1105,878],{"class":477},[467,1107,1108],{"class":487}," events",[467,1110,547],{"class":477},[467,1112,742],{"class":487},[467,1114,1115],{"class":477},"{\n",[467,1117,1119,1122,1125,1127,1129,1132],{"class":469,"line":1118},12,[467,1120,1121],{"class":690},"            OrderID",[467,1123,1124],{"class":477},":",[467,1126,974],{"class":690},[467,1128,547],{"class":477},[467,1130,1131],{"class":690},"ID",[467,1133,1134],{"class":477},",\n",[467,1136,1138,1141,1143,1146,1148,1151],{"class":469,"line":1137},13,[467,1139,1140],{"class":690},"            Email",[467,1142,1124],{"class":477},[467,1144,1145],{"class":690},"   order",[467,1147,547],{"class":477},[467,1149,1150],{"class":690},"Email",[467,1152,1134],{"class":477},[467,1154,1156],{"class":469,"line":1155},14,[467,1157,1158],{"class":477},"        })\n",[467,1160,1162],{"class":469,"line":1161},15,[467,1163,1164],{"class":477},"    })\n",[467,1166,1168,1171,1173,1175],{"class":469,"line":1167},16,[467,1169,1170],{"class":473},"    return",[467,1172,974],{"class":690},[467,1174,878],{"class":477},[467,1176,1022],{"class":690},[467,1178,1180],{"class":469,"line":1179},17,[467,1181,725],{"class":477},[447,1183,1184,1185,1188],{},"Three implementations satisfy the ",[451,1186,1187],{},"events.Dispatcher"," interface:",[1190,1191,1192,1208],"table",{},[1193,1194,1195],"thead",{},[1196,1197,1198,1202,1205],"tr",{},[1199,1200,1201],"th",{},"Constructor",[1199,1203,1204],{},"Behavior",[1199,1206,1207],{},"When",[1209,1210,1211,1239,1258],"tbody",{},[1196,1212,1213,1221,1232],{},[1214,1215,1216],"td",{},[514,1217,1218],{"href":73},[451,1219,1220],{},"outbox.NewDispatcher(store)",[1214,1222,1223,1224,1227,1228,1231],{},"writes the event into the ",[451,1225,1226],{},"outbox_events"," table ",[520,1229,1230],{},"in the caller's transaction","; a relay forwards it to Valkey",[1214,1233,1234,1235,1238],{},"state-changing flows, ",[520,1236,1237],{},"the generated default",", and what the shop uses",[1196,1240,1241,1246,1255],{},[1214,1242,1243],{},[451,1244,1245],{},"events.NewDispatcher(client)",[1214,1247,1248,1249,745],{},"enqueues directly to Valkey (through a ",[514,1250,1252],{"href":1251},"\u002Fen\u002Fqueue\u002Foverview#client",[451,1253,1254],{},"queue.Client",[1214,1256,1257],{},"fire-and-forget: if the process dies between the DB commit and the enqueue, the event is lost, fine where that's acceptable",[1196,1259,1260,1265,1271],{},[1214,1261,1262],{},[451,1263,1264],{},"events.NewSyncDispatcher(reg)",[1214,1266,1267,1268],{},"runs the registered listeners inline, in the caller's goroutine and transaction; listener errors come back via ",[451,1269,1270],{},"errors.Join",[1214,1272,1273],{},"unit tests and local tooling",[447,1275,1276,1277,547],{},"Why the outbox is the default, and what's wrong with a direct enqueue on a state change, is spelled out on the ",[514,1278,1279],{"href":73},"outbox page",[549,1281,1283],{"id":1282},"the-envelope-and-the-delivery-metadata","The envelope and the delivery metadata",[447,1285,1286,1287,1290,1291,1297,1298,1300],{},"On dispatch, ",[451,1288,1289],{},"events.NewEnvelope(ctx, ev)"," builds a ",[514,1292,1294],{"href":1293},"\u002Fen\u002Fqueue\u002Ftasks#the-envelope",[451,1295,1296],{},"queue.Envelope",": a fresh UUID (",[451,1299,1131],{},"), the event name, the JSON payload, the context's trace context and a timestamp. Whichever dispatcher you use, the listener receives the same envelope.",[447,1302,1303,1304,1124],{},"Before invoking a listener, the worker puts delivery metadata from the envelope onto the context: the listener reads it with ",[451,1305,1306],{},"events.MetaFromContext(ctx)",[1190,1308,1309,1319],{},[1193,1310,1311],{},[1196,1312,1313,1316],{},[1199,1314,1315],{},"Field",[1199,1317,1318],{},"Meaning",[1209,1320,1321,1334,1344,1354],{},[1196,1322,1323,1327],{},[1214,1324,1325],{},[451,1326,1131],{},[1214,1328,1329,1330,1333],{},"the envelope id, a ",[520,1331,1332],{},"stable idempotency key"," across retries and across all listeners of the same event",[1196,1335,1336,1341],{},[1214,1337,1338],{},[451,1339,1340],{},"Name",[1214,1342,1343],{},"the event name",[1196,1345,1346,1351],{},[1214,1347,1348],{},[451,1349,1350],{},"OccurredAt",[1214,1352,1353],{},"when the event was dispatched",[1196,1355,1356,1361],{},[1214,1357,1358],{},[451,1359,1360],{},"Attempt",[1214,1362,1363],{},"which attempt this is (0 on first delivery)",[549,1365,1367],{"id":1366},"listeners","Listeners",[447,1369,1370,1371,1374,1375,1378,1379,1124],{},"Listeners are registered with the generic ",[451,1372,1373],{},"events.Listen[T]",": the event type comes from the handler's signature, no manual type assertion. ",[451,1376,1377],{},"add listener shop orderPlaced sendOrderConfirmation --queue mail"," generates two things. The registration line in the module's ",[451,1380,1381],{},"listeners\u002Fregister.go",[458,1383,1386],{"className":460,"code":1384,"filename":1385,"language":462,"meta":463,"style":463},"package listeners\n\nimport (\n    \"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop\"\n\n    \"github.com\u002Fgp-system\u002Fevents\"\n    \"github.com\u002Fgp-system\u002Fqueue\"\n    \u002F\u002F gpsystem:worker-imports\n)\n\nfunc Register(reg *events.Registry, deps shop.Dependencies) {\n    events.Listen(reg, \"sendOrderConfirmation\", sendOrderConfirmation(deps), queue.OnQueue(\"mail\"))\n    \u002F\u002F gpsystem:listeners\n}\n","internal\u002Fmodules\u002Fshop\u002Flisteners\u002Fregister.go",[451,1387,1388,1395,1399,1405,1414,1418,1426,1434,1439,1443,1447,1485,1539,1544],{"__ignoreMap":463},[467,1389,1390,1392],{"class":469,"line":470},[467,1391,662],{"class":477},[467,1393,1394],{"class":487}," listeners\n",[467,1396,1397],{"class":469,"line":481},[467,1398,671],{"emptyLinePlaceholder":670},[467,1400,1401,1403],{"class":469,"line":494},[467,1402,474],{"class":473},[467,1404,478],{"class":477},[467,1406,1407,1409,1412],{"class":469,"line":504},[467,1408,484],{"class":477},[467,1410,1411],{"class":487},"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop",[467,1413,491],{"class":477},[467,1415,1416],{"class":469,"line":707},[467,1417,671],{"emptyLinePlaceholder":670},[467,1419,1420,1422,1424],{"class":469,"line":722},[467,1421,484],{"class":477},[467,1423,488],{"class":487},[467,1425,491],{"class":477},[467,1427,1428,1430,1432],{"class":469,"line":728},[467,1429,484],{"class":477},[467,1431,499],{"class":487},[467,1433,491],{"class":477},[467,1435,1436],{"class":469,"line":733},[467,1437,1438],{"class":572},"    \u002F\u002F gpsystem:worker-imports\n",[467,1440,1441],{"class":469,"line":1073},[467,1442,507],{"class":477},[467,1444,1445],{"class":469,"line":1080},[467,1446,671],{"emptyLinePlaceholder":670},[467,1448,1449,1451,1454,1456,1459,1462,1464,1466,1469,1471,1474,1477,1479,1481,1483],{"class":469,"line":1085},[467,1450,736],{"class":477},[467,1452,1453],{"class":748}," Register",[467,1455,864],{"class":477},[467,1457,1458],{"class":849},"reg",[467,1460,1461],{"class":477}," *",[467,1463,61],{"class":487},[467,1465,547],{"class":477},[467,1467,1468],{"class":487},"Registry",[467,1470,878],{"class":477},[467,1472,1473],{"class":849}," deps",[467,1475,1476],{"class":487}," shop",[467,1478,547],{"class":477},[467,1480,821],{"class":487},[467,1482,745],{"class":477},[467,1484,685],{"class":477},[467,1486,1487,1490,1492,1494,1496,1498,1500,1502,1505,1507,1509,1512,1514,1517,1520,1523,1525,1528,1530,1532,1534,1536],{"class":469,"line":1118},[467,1488,1489],{"class":690},"    events",[467,1491,547],{"class":477},[467,1493,806],{"class":748},[467,1495,864],{"class":477},[467,1497,1458],{"class":690},[467,1499,878],{"class":477},[467,1501,764],{"class":477},[467,1503,1504],{"class":565},"sendOrderConfirmation",[467,1506,770],{"class":477},[467,1508,878],{"class":477},[467,1510,1511],{"class":748}," sendOrderConfirmation",[467,1513,864],{"class":477},[467,1515,1516],{"class":690},"deps",[467,1518,1519],{"class":477},"),",[467,1521,1522],{"class":690}," queue",[467,1524,547],{"class":477},[467,1526,1527],{"class":748},"OnQueue",[467,1529,864],{"class":477},[467,1531,770],{"class":477},[467,1533,113],{"class":565},[467,1535,770],{"class":477},[467,1537,1538],{"class":477},"))\n",[467,1540,1541],{"class":469,"line":1137},[467,1542,1543],{"class":572},"    \u002F\u002F gpsystem:listeners\n",[467,1545,1546],{"class":469,"line":1155},[467,1547,725],{"class":477},[447,1549,1550],{},"And the listener stub in its own file: the body is yours; here is the shop's filled-in version:",[458,1552,1555],{"className":460,"code":1553,"filename":1554,"language":462,"meta":463,"style":463},"package listeners\n\nimport (\n    \"context\"\n\n    \"github.com\u002Fgp-system\u002Fmail\"\n    \"github.com\u002Fgp-system\u002Fmail\u002Fmjml\"\n\n    \"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop\"\n    shopevents \"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop\u002Fevents\"\n)\n\nfunc sendOrderConfirmation(deps shop.Dependencies) func(context.Context, shopevents.OrderPlaced) error {\n    return func(ctx context.Context, ev shopevents.OrderPlaced) error {\n        msg := mail.NewMessage().\n            WithTo(ev.Email).\n            WithSubject(\"Your order confirmation\").\n            WithBody(mjml.Template(templates, \"templates\u002Forder_confirmation.mjml.tmpl\",\n                map[string]any{\"OrderID\": ev.OrderID}))\n        return deps.Mailer.Send(ctx, msg)\n    }\n}\n","internal\u002Fmodules\u002Fshop\u002Flisteners\u002Fsend_order_confirmation.go",[451,1556,1557,1563,1567,1573,1582,1586,1595,1604,1608,1616,1628,1632,1636,1677,1708,1726,1743,1759,1791,1826,1854,1860],{"__ignoreMap":463},[467,1558,1559,1561],{"class":469,"line":470},[467,1560,662],{"class":477},[467,1562,1394],{"class":487},[467,1564,1565],{"class":469,"line":481},[467,1566,671],{"emptyLinePlaceholder":670},[467,1568,1569,1571],{"class":469,"line":494},[467,1570,474],{"class":473},[467,1572,478],{"class":477},[467,1574,1575,1577,1580],{"class":469,"line":504},[467,1576,484],{"class":477},[467,1578,1579],{"class":487},"context",[467,1581,491],{"class":477},[467,1583,1584],{"class":469,"line":707},[467,1585,671],{"emptyLinePlaceholder":670},[467,1587,1588,1590,1593],{"class":469,"line":722},[467,1589,484],{"class":477},[467,1591,1592],{"class":487},"github.com\u002Fgp-system\u002Fmail",[467,1594,491],{"class":477},[467,1596,1597,1599,1602],{"class":469,"line":728},[467,1598,484],{"class":477},[467,1600,1601],{"class":487},"github.com\u002Fgp-system\u002Fmail\u002Fmjml",[467,1603,491],{"class":477},[467,1605,1606],{"class":469,"line":733},[467,1607,671],{"emptyLinePlaceholder":670},[467,1609,1610,1612,1614],{"class":469,"line":1073},[467,1611,484],{"class":477},[467,1613,1411],{"class":487},[467,1615,491],{"class":477},[467,1617,1618,1621,1623,1626],{"class":469,"line":1080},[467,1619,1620],{"class":690},"    shopevents ",[467,1622,770],{"class":477},[467,1624,1625],{"class":487},"github.com\u002Facme\u002Fshop\u002Finternal\u002Fmodules\u002Fshop\u002Fevents",[467,1627,491],{"class":477},[467,1629,1630],{"class":469,"line":1085},[467,1631,507],{"class":477},[467,1633,1634],{"class":469,"line":1118},[467,1635,671],{"emptyLinePlaceholder":670},[467,1637,1638,1640,1642,1644,1646,1648,1650,1652,1654,1656,1658,1660,1662,1664,1667,1669,1671,1673,1675],{"class":469,"line":1137},[467,1639,736],{"class":477},[467,1641,1511],{"class":748},[467,1643,864],{"class":477},[467,1645,1516],{"class":849},[467,1647,1476],{"class":487},[467,1649,547],{"class":477},[467,1651,821],{"class":487},[467,1653,745],{"class":477},[467,1655,941],{"class":477},[467,1657,1579],{"class":487},[467,1659,547],{"class":477},[467,1661,875],{"class":487},[467,1663,878],{"class":477},[467,1665,1666],{"class":487}," shopevents",[467,1668,547],{"class":477},[467,1670,742],{"class":487},[467,1672,745],{"class":477},[467,1674,896],{"class":694},[467,1676,685],{"class":477},[467,1678,1679,1681,1683,1685,1687,1689,1691,1693,1696,1698,1700,1702,1704,1706],{"class":469,"line":1155},[467,1680,1170],{"class":473},[467,1682,941],{"class":477},[467,1684,867],{"class":849},[467,1686,870],{"class":487},[467,1688,547],{"class":477},[467,1690,875],{"class":487},[467,1692,878],{"class":477},[467,1694,1695],{"class":849}," ev",[467,1697,1666],{"class":487},[467,1699,547],{"class":477},[467,1701,742],{"class":487},[467,1703,745],{"class":477},[467,1705,896],{"class":694},[467,1707,685],{"class":477},[467,1709,1710,1713,1715,1718,1720,1723],{"class":469,"line":1161},[467,1711,1712],{"class":690},"        msg ",[467,1714,919],{"class":477},[467,1716,1717],{"class":690}," mail",[467,1719,547],{"class":477},[467,1721,1722],{"class":748},"NewMessage",[467,1724,1725],{"class":477},"().\n",[467,1727,1728,1731,1733,1736,1738,1740],{"class":469,"line":1167},[467,1729,1730],{"class":748},"            WithTo",[467,1732,864],{"class":477},[467,1734,1735],{"class":690},"ev",[467,1737,547],{"class":477},[467,1739,1150],{"class":690},[467,1741,1742],{"class":477},").\n",[467,1744,1745,1748,1750,1752,1755,1757],{"class":469,"line":1179},[467,1746,1747],{"class":748},"            WithSubject",[467,1749,864],{"class":477},[467,1751,770],{"class":477},[467,1753,1754],{"class":565},"Your order confirmation",[467,1756,770],{"class":477},[467,1758,1742],{"class":477},[467,1760,1762,1765,1767,1770,1772,1775,1777,1780,1782,1784,1787,1789],{"class":469,"line":1761},18,[467,1763,1764],{"class":748},"            WithBody",[467,1766,864],{"class":477},[467,1768,1769],{"class":690},"mjml",[467,1771,547],{"class":477},[467,1773,1774],{"class":748},"Template",[467,1776,864],{"class":477},[467,1778,1779],{"class":690},"templates",[467,1781,878],{"class":477},[467,1783,764],{"class":477},[467,1785,1786],{"class":565},"templates\u002Forder_confirmation.mjml.tmpl",[467,1788,770],{"class":477},[467,1790,1134],{"class":477},[467,1792,1794,1797,1799,1802,1805,1808,1810,1813,1815,1817,1819,1821,1823],{"class":469,"line":1793},19,[467,1795,1796],{"class":477},"                map[",[467,1798,695],{"class":694},[467,1800,1801],{"class":477},"]",[467,1803,1804],{"class":487},"any",[467,1806,1807],{"class":477},"{",[467,1809,770],{"class":477},[467,1811,1812],{"class":565},"OrderID",[467,1814,770],{"class":477},[467,1816,1124],{"class":477},[467,1818,1695],{"class":690},[467,1820,547],{"class":477},[467,1822,1812],{"class":690},[467,1824,1825],{"class":477},"}))\n",[467,1827,1829,1831,1833,1835,1838,1840,1843,1845,1847,1849,1852],{"class":469,"line":1828},20,[467,1830,1088],{"class":473},[467,1832,1473],{"class":690},[467,1834,547],{"class":477},[467,1836,1837],{"class":690},"Mailer",[467,1839,547],{"class":477},[467,1841,1842],{"class":748},"Send",[467,1844,864],{"class":477},[467,1846,867],{"class":690},[467,1848,878],{"class":477},[467,1850,1851],{"class":690}," msg",[467,1853,507],{"class":477},[467,1855,1857],{"class":469,"line":1856},21,[467,1858,1859],{"class":477},"    }\n",[467,1861,1863],{"class":469,"line":1862},22,[467,1864,725],{"class":477},[447,1866,1867,1868,1870,1871,1873,1874,1877],{},"(For the ",[451,1869,113],{},"\u002F",[451,1872,1769],{}," API, see the ",[514,1875,1876],{"href":119},"Mail page",".)",[447,1879,1880],{},"What to know about the registration:",[1882,1883,1884,1898,1928],"ul",{},[1885,1886,814,1887,1890,1891,1893,1894,1897],"li",{},[520,1888,1889],{},"name"," (the 2nd argument of ",[451,1892,806],{},") is unique per event; it forms the ",[451,1895,1896],{},"listener:shop.orderPlaced:sendOrderConfirmation"," task type, visible in the worker's logs and in Asynqmon. Registering the same (event, name) pair twice panics at startup.",[1885,1899,1900,1901,1908,1909,1912,1913,1915,1916,1920,1921,605,1924,1927],{},"The remaining arguments are ",[520,1902,1903,1904],{},"per-listener ",[514,1905,1907],{"href":1906},"\u002Fen\u002Fqueue\u002Ftasks#enqueue-options","enqueue options",": ",[451,1910,1911],{},"queue.OnQueue"," for the queue (the shop's mail goes onto the ",[451,1914,113],{}," queue so the ",[514,1917,1919],{"href":1918},"\u002Fen\u002Fkit\u002Fworker#queue-priorities","worker can weight it","), ",[451,1922,1923],{},"queue.MaxRetry",[451,1925,1926],{},"queue.Timeout"," for the retry budget.",[1885,1929,1930,1931,1934,1935,1937],{},"To subscribe to another module's event, use ",[451,1932,1933],{},"add listener \u003Cmodule> \u003Cproducer>.\u003Cevent> \u003Cname>","; you import the event type from the producer's ",[451,1936,61],{}," package.",[447,1939,1940,1942,1943,605,1946,589,1949,1952,1953,1957,1958,1961,1962,1965,1966,1969,1970,1973],{},[451,1941,1468],{}," also exposes ",[451,1944,1945],{},"EventNames() []string",[451,1947,1948],{},"FanoutTask(env queue.Envelope) ([]FanoutTask, error)",[451,1950,1951],{},"Handler(taskType string) (func(context.Context, queue.Envelope) error, bool)",": the pieces the ",[514,1954,1956],{"href":1955},"\u002Fen\u002Fkit\u002Fworker#what-processes-the-tasks","worker chassis"," uses to expand an ",[451,1959,1960],{},"event:"," task into its listener tasks and route a ",[451,1963,1964],{},"listener:"," task to the right function. Reach for them directly only if you're wiring a consumer without the kit's ",[451,1967,1968],{},"worker"," package; with the kit, ",[451,1971,1972],{},"worker.Run"," already does this for you.",[549,1975,1977],{"id":1976},"fan-out-how-one-event-becomes-n-tasks","Fan-out: how one event becomes N tasks",[447,1979,1980,1981,1983,1984,1991,1992,739,1995,1998,1999,2004],{},"The worker expands the ",[451,1982,783],{}," task and enqueues ",[520,1985,1986,1987,1990],{},"one ",[451,1988,1989],{},"listener:..."," task per listener",". Every listener task gets a deterministic ",[451,1993,1994],{},"TaskID",[451,1996,1997],{},"\u003Cenvelope-id>:\u003Clistener-name>","), so if the fan-out is interrupted and re-runs, the already-submitted tasks are no-ops via ",[514,2000,2001],{"href":1251},[451,2002,2003],{},"ErrDuplicate",": the fan-out itself is idempotent.",[447,2006,2007],{},"From there each listener lives its own life: it can sit on a different queue, it burns its own retry budget, and its failure re-runs only itself. This always-async-by-default behavior (rather than a synchronous event loop) is the defining trait of the package, and its price is the idempotency requirement.",[549,2009,2011],{"id":2010},"idempotency","Idempotency",[447,2013,2014,2015,2018,2019,2022],{},"With at-least-once delivery, a listener ",[520,2016,2017],{},"can run more than once for the same event"," (a retry, a worker crash mid-processing, a relay re-send). ",[451,2020,2021],{},"Meta.ID"," is the stable key for this: it is identical on every attempt of the same dispatch.",[458,2024,2026],{"className":460,"code":2025,"language":462,"meta":463,"style":463},"meta, _ := events.MetaFromContext(ctx)\n\u002F\u002F e.g. a unique key in a \"did this happen already?\" table, or the mail\n\u002F\u002F provider's idempotency key:\nreturn deps.Mailer.SendOnce(ctx, meta.ID, msg)\n",[451,2027,2028,2053,2058,2063],{"__ignoreMap":463},[467,2029,2030,2033,2035,2038,2040,2042,2044,2047,2049,2051],{"class":469,"line":470},[467,2031,2032],{"class":690},"meta",[467,2034,878],{"class":477},[467,2036,2037],{"class":690}," _ ",[467,2039,919],{"class":477},[467,2041,1108],{"class":690},[467,2043,547],{"class":477},[467,2045,2046],{"class":748},"MetaFromContext",[467,2048,864],{"class":477},[467,2050,867],{"class":690},[467,2052,507],{"class":477},[467,2054,2055],{"class":469,"line":481},[467,2056,2057],{"class":572},"\u002F\u002F e.g. a unique key in a \"did this happen already?\" table, or the mail\n",[467,2059,2060],{"class":469,"line":494},[467,2061,2062],{"class":572},"\u002F\u002F provider's idempotency key:\n",[467,2064,2065,2068,2070,2072,2074,2076,2079,2081,2083,2085,2088,2090,2092,2094,2096],{"class":469,"line":504},[467,2066,2067],{"class":473},"return",[467,2069,1473],{"class":690},[467,2071,547],{"class":477},[467,2073,1837],{"class":690},[467,2075,547],{"class":477},[467,2077,2078],{"class":748},"SendOnce",[467,2080,864],{"class":477},[467,2082,867],{"class":690},[467,2084,878],{"class":477},[467,2086,2087],{"class":690}," meta",[467,2089,547],{"class":477},[467,2091,1131],{"class":690},[467,2093,878],{"class":477},[467,2095,1851],{"class":690},[467,2097,507],{"class":477},[447,2099,2100],{},"Practical patterns:",[1882,2102,2103,2113,2123],{},[1885,2104,2105,2108,2109,2112],{},[520,2106,2107],{},"Natural key",": when the side effect is a DB write, ",[451,2110,2111],{},"INSERT ... ON CONFLICT DO NOTHING"," on the event id (or the business key).",[1885,2114,2115,2118,2119,2122],{},[520,2116,2117],{},"External API",": pass ",[451,2120,2121],{},"meta.ID"," as an idempotency-key header when the provider supports one.",[1885,2124,2125,2128,2129,547],{},[520,2126,2127],{},"Read + condition",": the race between checking \"did we already send it?\" and the send is only closed by a key-based approach; don't settle for a bare ",[451,2130,2131],{},"SELECT",[549,2133,2135],{"id":2134},"where-listeners-run","Where listeners run",[447,2137,2138,2139,2142,2143,2145,2146,2149,2150,2153,2154,2157,2158,2161,2162,605,2164,2167,2168,547],{},"Listeners run in the ",[514,2140,2141],{"href":263},"worker binary",": the per-module ",[451,2144,1366],{}," subpackages' ",[451,2147,2148],{},"Register"," functions are composed into a single ",[451,2151,2152],{},"events.Registry"," by the generated ",[451,2155,2156],{},"cmd\u002Fworker\u002Fmain.go",". For scheduled event firing (events dispatched on a cron), see the ",[514,2159,2160],{"href":78},"scheduler","; for the generator commands (",[451,2163,825],{},[451,2165,2166],{},"add listener","), the ",[514,2169,2170],{"href":349},"CLI worker generators",[549,2172,2174],{"id":2173},"with-the-kit","With the kit",[447,2176,2177,2179,2180,2182,2183,605,2185,589,2187,2190,2191,1870,2193,2196,2197,2199,2200,2204,2205,2209,2210,2212,2213,2216],{},[451,2178,319],{}," wires ",[451,2181,61],{}," in from the start: ",[451,2184,825],{},[451,2186,2166],{},[451,2188,2189],{},"add job"," generate the ",[451,2192,1468],{},[451,2194,2195],{},"Schedule"," composition shown above, and the ",[514,2198,1956],{"href":263}," is what actually runs it (its own asynq server, plus the ",[514,2201,2203],{"href":2202},"\u002Fen\u002Fevents\u002Foutbox#the-relay","outbox relay"," and the ",[514,2206,2208],{"href":2207},"\u002Fen\u002Fevents\u002Fscheduler#replica-safety-the-leader-election","schedule runner"," alongside it). None of that is required to use ",[451,2211,61],{}," standalone: outside the kit you build your own asynq server and register ",[451,2214,2215],{},"reg.Handler(taskType)"," for the task types you care about.",[549,2218,2220],{"id":2219},"patterns-used","Patterns used",[447,2222,2223,739,2226,2228,2229,2231,2232,547],{},[520,2224,2225],{},"At-least-once delivery + idempotent listener contract",[451,2227,2021],{}," as the dedup key, deterministic ",[451,2230,1994],{}," in fan-out): see ",[514,2233,238],{"href":2234},"\u002Fen\u002Fconcepts\u002Fdesign-patterns#at-least-once-delivery--idempotent-listener",[2236,2237,2238],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"title":463,"searchDepth":481,"depth":481,"links":2240},[2241,2242,2243,2244,2245,2246,2247,2248,2249,2250],{"id":551,"depth":481,"text":25},{"id":635,"depth":481,"text":636},{"id":810,"depth":481,"text":811},{"id":1282,"depth":481,"text":1283},{"id":1366,"depth":481,"text":1367},{"id":1976,"depth":481,"text":1977},{"id":2010,"depth":481,"text":2011},{"id":2134,"depth":481,"text":2135},{"id":2173,"depth":481,"text":2174},{"id":2219,"depth":481,"text":2220},"Events and listeners over asynq, every listener is an independent task with its own retry budget.","md",null,{},{"icon":70},{"title":67,"description":2251},"8Qn9uP8PjYl8lDuB-uoZeG05knRemUwJzOr5JJBMN7Q",[2259,2261],{"title":57,"path":58,"stem":59,"description":2260,"icon":47,"children":-1},"Enqueue options, the event envelope, OTel trace propagation across the queue, and task-type naming conventions.",{"title":72,"path":73,"stem":74,"description":2262,"icon":75,"children":-1},"The transactional outbox pattern, the business write and the event commit atomically, with a delivery guarantee.",1785445889150]