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