[{"data":1,"prerenderedAt":2712},["ShallowReactive",2],{"navigation":3,"\u002Fdocs\u002Fpackages\u002Fmessaging\u002Fredis":176,"\u002Fdocs\u002Fpackages\u002Fmessaging\u002Fredis-surround":2707},[4,21,86,158],{"title":5,"path":6,"stem":7,"children":8,"status":11,"icon":20},"Getting Started","\u002Fdocs\u002Fgetting-started","1.docs\u002F1.getting-started\u002F1.index",[9,12,16],{"title":10,"path":6,"stem":7,"status":11},"Introduction",null,{"title":13,"path":14,"stem":15,"status":11},"Installation","\u002Fdocs\u002Fgetting-started\u002Finstallation","1.docs\u002F1.getting-started\u002F2.installation",{"title":17,"path":18,"stem":19,"status":11},"Quick Start","\u002Fdocs\u002Fgetting-started\u002Fquick-start","1.docs\u002F1.getting-started\u002F3.quick-start",false,{"title":22,"path":23,"stem":24,"children":25,"status":11,"icon":20},"Core Concepts","\u002Fdocs\u002Fcore-concepts","1.docs\u002F2.core-concepts\u002F1.index",[26,28,32,36,46,50,54,58,62,66,70,74,78,82],{"title":27,"path":23,"stem":24,"status":11},"Overview",{"title":29,"path":30,"stem":31,"status":11},"Response","\u002Fdocs\u002Fcore-concepts\u002Fresponse","1.docs\u002F2.core-concepts\u002F10.response",{"title":33,"path":34,"stem":35,"status":11},"Testing","\u002Fdocs\u002Fcore-concepts\u002Ftesting","1.docs\u002F2.core-concepts\u002F12.testing",{"title":37,"path":38,"stem":39,"children":40,"status":11,"icon":20,"defaultOpen":20},"Decorators","\u002Fdocs\u002Fcore-concepts\u002Fdecorators","1.docs\u002F2.core-concepts\u002F13.decorators\u002F1.index",[41,42],{"title":27,"path":38,"stem":39,"status":11},{"title":43,"path":44,"stem":45,"status":11},"Custom","\u002Fdocs\u002Fcore-concepts\u002Fdecorators\u002Fcustom","1.docs\u002F2.core-concepts\u002F13.decorators\u002F2.custom",{"title":47,"path":48,"stem":49,"status":11},"Discovery Service","\u002Fdocs\u002Fcore-concepts\u002Fdiscovery","1.docs\u002F2.core-concepts\u002F14.discovery",{"title":51,"path":52,"stem":53,"status":11},"Application Lifecycle","\u002Fdocs\u002Fcore-concepts\u002Fapp-lifecycle","1.docs\u002F2.core-concepts\u002F15.app-lifecycle",{"title":55,"path":56,"stem":57,"status":11},"Controllers","\u002Fdocs\u002Fcore-concepts\u002Fcontrollers","1.docs\u002F2.core-concepts\u002F2.controllers",{"title":59,"path":60,"stem":61,"status":11},"Routing","\u002Fdocs\u002Fcore-concepts\u002Frouting","1.docs\u002F2.core-concepts\u002F3.routing",{"title":63,"path":64,"stem":65,"status":11},"Providers","\u002Fdocs\u002Fcore-concepts\u002Fproviders","1.docs\u002F2.core-concepts\u002F4.providers",{"title":67,"path":68,"stem":69,"status":11},"Modules","\u002Fdocs\u002Fcore-concepts\u002Fmodules","1.docs\u002F2.core-concepts\u002F5.modules",{"title":71,"path":72,"stem":73,"status":11},"Configuration","\u002Fdocs\u002Fcore-concepts\u002Fconfiguration","1.docs\u002F2.core-concepts\u002F6.configuration",{"title":75,"path":76,"stem":77,"status":11},"Middleware","\u002Fdocs\u002Fcore-concepts\u002Fmiddleware","1.docs\u002F2.core-concepts\u002F7.middleware",{"title":79,"path":80,"stem":81,"status":11},"Guards","\u002Fdocs\u002Fcore-concepts\u002Fguards","1.docs\u002F2.core-concepts\u002F8.guards",{"title":83,"path":84,"stem":85,"status":11},"Exceptions","\u002Fdocs\u002Fcore-concepts\u002Fexceptions","1.docs\u002F2.core-concepts\u002F9.exceptions",{"title":87,"path":88,"stem":89,"children":90,"status":11,"icon":20},"Packages","\u002Fdocs\u002Fpackages","1.docs\u002F3.packages\u002F1.index",[91,92,97,108,112,130,134,138,142,146,150,154],{"title":27,"path":88,"stem":89,"status":11},{"title":93,"path":94,"stem":95,"status":96},"CLI","\u002Fdocs\u002Fpackages\u002Fcli","1.docs\u002F3.packages\u002F10.cli","experimental",{"title":98,"path":99,"stem":100,"children":101,"status":11,"icon":20,"defaultOpen":20},"Events","\u002Fdocs\u002Fpackages\u002Fmessaging","1.docs\u002F3.packages\u002F11.messaging\u002F1.index",[102,104],{"title":27,"path":99,"stem":100,"status":103},"beta",{"title":105,"path":106,"stem":107,"status":96},"Redis","\u002Fdocs\u002Fpackages\u002Fmessaging\u002Fredis","1.docs\u002F3.packages\u002F11.messaging\u002F2.redis",{"title":109,"path":110,"stem":111,"status":103},"Serve Static","\u002Fdocs\u002Fpackages\u002Fserve-static","1.docs\u002F3.packages\u002F12.serve-static",{"title":113,"path":114,"stem":115,"children":116,"status":11,"icon":20,"defaultOpen":20},"Auth","\u002Fdocs\u002Fpackages\u002Fauth","1.docs\u002F3.packages\u002F2.auth\u002F1.index",[117,118,122,126],{"title":27,"path":114,"stem":115,"status":103},{"title":119,"path":120,"stem":121,"status":103},"JWT Provider","\u002Fdocs\u002Fpackages\u002Fauth\u002Fjwt","1.docs\u002F3.packages\u002F2.auth\u002F2.jwt",{"title":123,"path":124,"stem":125,"status":103},"Local Provider","\u002Fdocs\u002Fpackages\u002Fauth\u002Flocal","1.docs\u002F3.packages\u002F2.auth\u002F3.local",{"title":127,"path":128,"stem":129,"status":96},"OAuth2 Provider","\u002Fdocs\u002Fpackages\u002Fauth\u002Foauth2","1.docs\u002F3.packages\u002F2.auth\u002F4.oauth2",{"title":131,"path":132,"stem":133,"status":103},"JWT","\u002Fdocs\u002Fpackages\u002Fjwt","1.docs\u002F3.packages\u002F3.jwt",{"title":135,"path":136,"stem":137,"status":103},"Drizzle","\u002Fdocs\u002Fpackages\u002Fdrizzle","1.docs\u002F3.packages\u002F4.drizzle",{"title":139,"path":140,"stem":141,"status":103},"Papr","\u002Fdocs\u002Fpackages\u002Fpapr","1.docs\u002F3.packages\u002F5.papr",{"title":143,"path":144,"stem":145,"status":103},"Mongoose","\u002Fdocs\u002Fpackages\u002Fmongoose","1.docs\u002F3.packages\u002F6.mongoose",{"title":147,"path":148,"stem":149,"status":103},"Swagger","\u002Fdocs\u002Fpackages\u002Fswagger","1.docs\u002F3.packages\u002F7.swagger",{"title":151,"path":152,"stem":153,"status":103},"Node Server","\u002Fdocs\u002Fpackages\u002Fnode-server","1.docs\u002F3.packages\u002F8.node-server",{"title":155,"path":156,"stem":157,"status":103},"uWS Server","\u002Fdocs\u002Fpackages\u002Fuws-server","1.docs\u002F3.packages\u002F9.uws-server",{"title":159,"path":160,"stem":161,"children":162,"status":11,"icon":20},"Roadmap","\u002Fdocs\u002Froadmap","1.docs\u002F4.roadmap\u002F1.index",[163,164,168,172],{"title":27,"path":160,"stem":161,"status":11},{"title":165,"path":166,"stem":167,"status":11},"Short term (0-3 months)","\u002Fdocs\u002Froadmap\u002Fshort-term","1.docs\u002F4.roadmap\u002F2.short-term",{"title":169,"path":170,"stem":171,"status":11},"Mid term (3-9 months)","\u002Fdocs\u002Froadmap\u002Fmid-term","1.docs\u002F4.roadmap\u002F3.mid-term",{"title":173,"path":174,"stem":175,"status":11},"Long term (9-12+ months)","\u002Fdocs\u002Froadmap\u002Flong-term","1.docs\u002F4.roadmap\u002F4.long-term",{"id":177,"title":105,"body":178,"description":2702,"extension":2703,"meta":2704,"navigation":386,"path":106,"seo":2705,"status":96,"stem":107,"__hash__":2706},"docs\u002F1.docs\u002F3.packages\u002F11.messaging\u002F2.redis.md",{"type":179,"value":180,"toc":2681},"minimark",[181,204,207,211,291,301,305,516,523,660,675,679,930,934,982,990,1000,1025,1046,1051,1078,1081,1085,1100,1104,1118,1124,1129,1133,1146,1152,1155,1159,1208,1378,1392,1408,1412,1415,1460,1483,1487,1490,1496,1499,1571,1589,1593,1596,1608,1615,1618,1632,1661,1679,1693,1696,1709,1916,1940,2017,2045,2048,2054,2173,2179,2221,2240,2243,2246,2257,2568,2571,2597,2603,2607,2677],[182,183,184,188,189,195,196,199,200,203],"p",{},[185,186,187],"code",{},"@miiajs\u002Fmessaging-redis"," is the production transport for ",[190,191,192],"a",{"href":99},[185,193,194],{},"@miiajs\u002Fmessaging",". It implements the same ",[185,197,198],{},"MessageTransport"," interface as the in-memory default, so user code does not change - only the transport factory passed to ",[185,201,202],{},"MessagingModule.configure",". Reach for it as soon as you need persistence, multi-replica deployments, or crash recovery.",[182,205,206],{},"Under the hood it uses Redis Streams with consumer groups, a ZSET-backed retry scheduler, and atomic Lua scripts for ack\u002Fnack\u002FDLQ transitions.",[208,209,13],"h2",{"id":210},"installation",[212,213,214,242,259,275],"code-group",{},[215,216,222],"pre",{"className":217,"code":218,"filename":219,"language":220,"meta":221,"style":221},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","bun add @miiajs\u002Fmessaging-redis ioredis\n","bun","bash","",[185,223,224],{"__ignoreMap":221},[225,226,229,232,236,239],"span",{"class":227,"line":228},"line",1,[225,230,219],{"class":231},"sBMFI",[225,233,235],{"class":234},"sfazB"," add",[225,237,238],{"class":234}," @miiajs\u002Fmessaging-redis",[225,240,241],{"class":234}," ioredis\n",[215,243,246],{"className":217,"code":244,"filename":245,"language":220,"meta":221,"style":221},"npm install @miiajs\u002Fmessaging-redis ioredis\n","npm",[185,247,248],{"__ignoreMap":221},[225,249,250,252,255,257],{"class":227,"line":228},[225,251,245],{"class":231},[225,253,254],{"class":234}," install",[225,256,238],{"class":234},[225,258,241],{"class":234},[215,260,263],{"className":217,"code":261,"filename":262,"language":220,"meta":221,"style":221},"pnpm add @miiajs\u002Fmessaging-redis ioredis\n","pnpm",[185,264,265],{"__ignoreMap":221},[225,266,267,269,271,273],{"class":227,"line":228},[225,268,262],{"class":231},[225,270,235],{"class":234},[225,272,238],{"class":234},[225,274,241],{"class":234},[215,276,279],{"className":217,"code":277,"filename":278,"language":220,"meta":221,"style":221},"yarn add @miiajs\u002Fmessaging-redis ioredis\n","yarn",[185,280,281],{"__ignoreMap":221},[225,282,283,285,287,289],{"class":227,"line":228},[225,284,278],{"class":231},[225,286,235],{"class":234},[225,288,238],{"class":234},[225,290,241],{"class":234},[182,292,293,296,297,300],{},[185,294,295],{},"ioredis"," is a peer dependency - install it explicitly so your app pins the version. The transport is tested against ",[185,298,299],{},"ioredis@^5",".",[208,302,304],{"id":303},"setup","Setup",[215,306,310],{"className":307,"code":308,"language":309,"meta":221,"style":221},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { Module } from '@miiajs\u002Fcore'\nimport { MessagingModule } from '@miiajs\u002Fmessaging'\nimport { redisStreamsTransport } from '@miiajs\u002Fmessaging-redis'\n\n@Module({\n  imports: [\n    MessagingModule.configure({\n      transport: redisStreamsTransport({\n        url: 'redis:\u002F\u002Flocalhost:6379',\n      }),\n    }),\n  ],\n})\nclass AppModule {}\n","typescript",[185,311,312,341,361,381,388,404,417,432,446,465,476,486,494,503],{"__ignoreMap":221},[225,313,314,318,322,326,329,332,335,338],{"class":227,"line":228},[225,315,317],{"class":316},"s7zQu","import",[225,319,321],{"class":320},"sMK4o"," {",[225,323,325],{"class":324},"sTEyZ"," Module",[225,327,328],{"class":320}," }",[225,330,331],{"class":316}," from",[225,333,334],{"class":320}," '",[225,336,337],{"class":234},"@miiajs\u002Fcore",[225,339,340],{"class":320},"'\n",[225,342,344,346,348,351,353,355,357,359],{"class":227,"line":343},2,[225,345,317],{"class":316},[225,347,321],{"class":320},[225,349,350],{"class":324}," MessagingModule",[225,352,328],{"class":320},[225,354,331],{"class":316},[225,356,334],{"class":320},[225,358,194],{"class":234},[225,360,340],{"class":320},[225,362,364,366,368,371,373,375,377,379],{"class":227,"line":363},3,[225,365,317],{"class":316},[225,367,321],{"class":320},[225,369,370],{"class":324}," redisStreamsTransport",[225,372,328],{"class":320},[225,374,331],{"class":316},[225,376,334],{"class":320},[225,378,187],{"class":234},[225,380,340],{"class":320},[225,382,384],{"class":227,"line":383},4,[225,385,387],{"emptyLinePlaceholder":386},true,"\n",[225,389,391,394,398,401],{"class":227,"line":390},5,[225,392,393],{"class":320},"@",[225,395,397],{"class":396},"s2Zo4","Module",[225,399,400],{"class":324},"(",[225,402,403],{"class":320},"{\n",[225,405,407,411,414],{"class":227,"line":406},6,[225,408,410],{"class":409},"swJcz","  imports",[225,412,413],{"class":320},":",[225,415,416],{"class":324}," [\n",[225,418,420,423,425,428,430],{"class":227,"line":419},7,[225,421,422],{"class":324},"    MessagingModule",[225,424,300],{"class":320},[225,426,427],{"class":396},"configure",[225,429,400],{"class":324},[225,431,403],{"class":320},[225,433,435,438,440,442,444],{"class":227,"line":434},8,[225,436,437],{"class":409},"      transport",[225,439,413],{"class":320},[225,441,370],{"class":396},[225,443,400],{"class":324},[225,445,403],{"class":320},[225,447,449,452,454,456,459,462],{"class":227,"line":448},9,[225,450,451],{"class":409},"        url",[225,453,413],{"class":320},[225,455,334],{"class":320},[225,457,458],{"class":234},"redis:\u002F\u002Flocalhost:6379",[225,460,461],{"class":320},"'",[225,463,464],{"class":320},",\n",[225,466,468,471,474],{"class":227,"line":467},10,[225,469,470],{"class":320},"      }",[225,472,473],{"class":324},")",[225,475,464],{"class":320},[225,477,479,482,484],{"class":227,"line":478},11,[225,480,481],{"class":320},"    }",[225,483,473],{"class":324},[225,485,464],{"class":320},[225,487,489,492],{"class":227,"line":488},12,[225,490,491],{"class":324},"  ]",[225,493,464],{"class":320},[225,495,497,500],{"class":227,"line":496},13,[225,498,499],{"class":320},"}",[225,501,502],{"class":324},")\n",[225,504,506,510,513],{"class":227,"line":505},14,[225,507,509],{"class":508},"spNyl","class",[225,511,512],{"class":231}," AppModule",[225,514,515],{"class":320}," {}\n",[182,517,518,519,522],{},"With ",[185,520,521],{},"ConfigService"," for env-driven URLs:",[215,524,526],{"className":307,"code":525,"language":309,"meta":221,"style":221},"import { ConfigService } from '@miiajs\u002Fconfig'\n\nMessagingModule.configure((resolve) => ({\n  transport: redisStreamsTransport({\n    url: resolve(ConfigService).getOrThrow('REDIS_URL'),\n    retry: { maxAttempts: 10 },\n  }),\n}))\n",[185,527,528,548,552,579,592,623,644,653],{"__ignoreMap":221},[225,529,530,532,534,537,539,541,543,546],{"class":227,"line":228},[225,531,317],{"class":316},[225,533,321],{"class":320},[225,535,536],{"class":324}," ConfigService",[225,538,328],{"class":320},[225,540,331],{"class":316},[225,542,334],{"class":320},[225,544,545],{"class":234},"@miiajs\u002Fconfig",[225,547,340],{"class":320},[225,549,550],{"class":227,"line":343},[225,551,387],{"emptyLinePlaceholder":386},[225,553,554,557,559,561,563,565,569,571,574,577],{"class":227,"line":363},[225,555,556],{"class":324},"MessagingModule",[225,558,300],{"class":320},[225,560,427],{"class":396},[225,562,400],{"class":324},[225,564,400],{"class":320},[225,566,568],{"class":567},"sHdIc","resolve",[225,570,473],{"class":320},[225,572,573],{"class":508}," =>",[225,575,576],{"class":324}," (",[225,578,403],{"class":320},[225,580,581,584,586,588,590],{"class":227,"line":383},[225,582,583],{"class":409},"  transport",[225,585,413],{"class":320},[225,587,370],{"class":396},[225,589,400],{"class":324},[225,591,403],{"class":320},[225,593,594,597,599,602,605,607,610,612,614,617,619,621],{"class":227,"line":390},[225,595,596],{"class":409},"    url",[225,598,413],{"class":320},[225,600,601],{"class":396}," resolve",[225,603,604],{"class":324},"(ConfigService)",[225,606,300],{"class":320},[225,608,609],{"class":396},"getOrThrow",[225,611,400],{"class":324},[225,613,461],{"class":320},[225,615,616],{"class":234},"REDIS_URL",[225,618,461],{"class":320},[225,620,473],{"class":324},[225,622,464],{"class":320},[225,624,625,628,630,632,635,637,641],{"class":227,"line":406},[225,626,627],{"class":409},"    retry",[225,629,413],{"class":320},[225,631,321],{"class":320},[225,633,634],{"class":409}," maxAttempts",[225,636,413],{"class":320},[225,638,640],{"class":639},"sbssI"," 10",[225,642,643],{"class":320}," },\n",[225,645,646,649,651],{"class":227,"line":419},[225,647,648],{"class":320},"  }",[225,650,473],{"class":324},[225,652,464],{"class":320},[225,654,655,657],{"class":227,"line":434},[225,656,499],{"class":320},[225,658,659],{"class":324},"))\n",[182,661,662,663,666,667,670,671,674],{},"Everything else from ",[190,664,665],{"href":99},"the Overview page"," - ",[185,668,669],{},"@On",", groups, retry\u002FDLQ, ",[185,672,673],{},"MessageBus.publish"," - works identically.",[208,676,678],{"id":677},"redisstreamstransport-options","redisStreamsTransport options",[680,681,682,701],"table",{},[683,684,685],"thead",{},[686,687,688,692,695,698],"tr",{},[689,690,691],"th",{},"Option",[689,693,694],{},"Type",[689,696,697],{},"Default",[689,699,700],{},"Description",[702,703,704,726,758,792,812,835,856,885,911],"tbody",{},[686,705,706,712,717,720],{},[707,708,709],"td",{},[185,710,711],{},"url",[707,713,714],{},[185,715,716],{},"string",[707,718,719],{},"-",[707,721,722,723,300],{},"Redis URL. Mutually exclusive with ",[185,724,725],{},"client",[686,727,728,732,736,738],{},[707,729,730],{},[185,731,725],{},[707,733,734],{},[185,735,105],{},[707,737,719],{},[707,739,740,741,745,746,749,750,753,754,300],{},"Pre-built ioredis instance. The transport does ",[742,743,744],"strong",{},"not"," call ",[185,747,748],{},"connect()"," or ",[185,751,752],{},"quit()"," on a user-supplied client - see ",[190,755,757],{"href":756},"#pre-built-ioredis-client","Pre-built ioredis client",[686,759,760,765,770,775],{},[707,761,762],{},[185,763,764],{},"retry",[707,766,767],{},[185,768,769],{},"Partial\u003CRetryConfig>",[707,771,772],{},[185,773,774],{},"DEFAULT_RETRY",[707,776,777,778,781,782,781,785,781,788,791],{},"Override individual retry fields (",[185,779,780],{},"maxAttempts",", ",[185,783,784],{},"backoffMs",[185,786,787],{},"backoffMultiplier",[185,789,790],{},"dlq",").",[686,793,794,799,804,809],{},[707,795,796],{},[185,797,798],{},"retrySchedulerIntervalMs",[707,800,801],{},[185,802,803],{},"number",[707,805,806],{},[185,807,808],{},"1000",[707,810,811],{},"How often the retry ZSET is drained back into the main stream.",[686,813,814,819,823,828],{},[707,815,816],{},[185,817,818],{},"reclaimIntervalMs",[707,820,821],{},[185,822,803],{},[707,824,825],{},[185,826,827],{},"30000",[707,829,830,831,834],{},"How often ",[185,832,833],{},"XAUTOCLAIM"," runs to recover pending entries from dead consumers.",[686,836,837,842,846,851],{},[707,838,839],{},[185,840,841],{},"minIdleMs",[707,843,844],{},[185,845,803],{},[707,847,848],{},[185,849,850],{},"60000",[707,852,853,854,300],{},"Minimum idle time before a pending entry is eligible for ",[185,855,833],{},[686,857,858,863,867,872],{},[707,859,860],{},[185,861,862],{},"blockMs",[707,864,865],{},[185,866,803],{},[707,868,869],{},[185,870,871],{},"5000",[707,873,874,877,878,880,881,300],{},[185,875,876],{},"XREADGROUP BLOCK"," timeout. Knob for idle Redis traffic and unsubscribe responsiveness only - does ",[742,879,744],{}," affect publish or read latency thanks to the ",[190,882,884],{"href":883},"#connection-model","connection model",[686,886,887,892,896,900],{},[707,888,889],{},[185,890,891],{},"drainTimeoutMs",[707,893,894],{},[185,895,803],{},[707,897,898],{},[185,899,871],{},[707,901,902,903,906,907,300],{},"Max time ",[185,904,905],{},"onDestroy()"," waits for in-flight handlers before closing the Redis client. See ",[190,908,910],{"href":909},"#graceful-drain","Graceful drain",[686,912,913,918,922,927],{},[707,914,915],{},[185,916,917],{},"consumerName",[707,919,920],{},[185,921,716],{},[707,923,924],{},[185,925,926],{},"${hostname}:${pid}:${rand8}",[707,928,929],{},"Override for the auto-generated consumer name. Useful for diagnostics.",[208,931,933],{"id":932},"connection-model","Connection model",[182,935,936,937,940,941,781,944,947,948,951,952,951,955,958,959,962,963,965,966,969,970,973,974,977,978,981],{},"The transport keeps ",[742,938,939],{},"one publisher client"," for ",[185,942,943],{},"XADD",[185,945,946],{},"XACK",", all Lua commands (",[185,949,950],{},"retrySchedule"," \u002F ",[185,953,954],{},"drainRetry",[185,956,957],{},"moveToDlq","), ",[185,960,961],{},"XGROUP CREATE",", and the ",[185,964,833],{}," housekeeping loop. Every ",[185,967,968],{},"subscribe()"," call additionally creates its ",[742,971,972],{},"own duplicated client"," via ",[185,975,976],{},"pubClient.duplicate({ lazyConnect: true })"," and runs its blocking ",[185,979,980],{},"XREADGROUP"," loop on it.",[215,983,988],{"className":984,"code":986,"language":987},[985],"language-text","┌─────────────────────────────────────────────────────┐\n│ pubClient            (1 socket)                     │\n│   ├─ XADD            (publish)                      │\n│   ├─ XACK            (per-message ack)              │\n│   ├─ Lua scripts     (retrySchedule, moveToDlq)     │\n│   └─ XAUTOCLAIM      (idle reclaim, drainRetry)     │\n├─────────────────────────────────────────────────────┤\n│ subClient #1         (1 socket per subscribe call)  │\n│   └─ XREADGROUP BLOCK \u003Cms>                          │\n│ subClient #2                                        │\n│   └─ XREADGROUP BLOCK \u003Cms>                          │\n│ ...                                                 │\n└─────────────────────────────────────────────────────┘\n","text",[185,989,986],{"__ignoreMap":221},[182,991,992,993,996,997,999],{},"Why split the connection: ioredis serializes commands on a single TCP socket, and ",[185,994,995],{},"XREADGROUP ... BLOCK \u003Cms>"," holds the socket until data arrives or the timeout fires. Sharing one connection for publishing and blocking subscribers would queue every ",[185,998,943],{}," behind the in-flight BLOCK, with publish latency growing linearly in subscriber count. Per-subscribe duplicates make publish latency constant (≈ network RTT), regardless of how many topics × groups you subscribe to. This is the same pattern BullMQ uses for its workers.",[182,1001,1002,1003,1006,1007,1009,1010,1013,1014,1017,1018,1021,1022,1024],{},"Connection count: ",[185,1004,1005],{},"1 (publisher) + Σ handlers × max(1, sliding lane count)",". Each ",[185,1008,669],{}," is its own subscription, so handler count is the multiplier - batch handlers add one connection each, sliding handlers add ",[185,1011,1012],{},"concurrency"," connections each. Replicas multiply this by replica count, but consumers within an auto-derived group share that group's broker round-robin (replicas don't multiply broker-side group count - just consumer count within each group). ",[185,1015,1016],{},"redisIdempotencyStore"," keeps its own client and adds to the count separately. SubClients are owned by the transport regardless of ",[185,1019,1020],{},"ownsClient"," - the ",[185,1023,1020],{}," flag only governs the user-supplied parent client; duplicates are always created and closed by us.",[1026,1027,1028],"warning",{},[182,1029,1030,1033,1034,1037,1038,1041,1042,1045],{},[742,1031,1032],{},"Cost warning for managed Redis tiers."," Combining bus-default sliding with many handlers is multiplicative. Example: 10 handlers and ",[185,1035,1036],{},"MessagingModule.configure({ dispatch: { mode: 'sliding', concurrency: 4 } })"," produces 41 connections per replica (1 publisher + 10 × 4 lanes). Managed providers like Upstash and Redis Cloud price by connection count. Recommendation: leave the bus default at ",[185,1039,1040],{},"batch",", opt into ",[185,1043,1044],{},"mode: 'sliding'"," only on individual handlers with variable runtime characteristics.",[1047,1048,1050],"h3",{"id":1049},"broadcast-group-lifecycle","Broadcast group lifecycle",[182,1052,1053,1054,1057,1058,1061,1062,1065,1066,1069,1070,1073,1074,1077],{},"Each handler with ",[185,1055,1056],{},"broadcast: true"," derives its consumer group as ",[185,1059,1060],{},"\u003Cbase>__\u003Chostname>_\u003Cpid>",". On graceful shutdown (",[185,1063,1064],{},"onDestroy","), the transport runs ",[185,1067,1068],{},"XGROUP DESTROY"," on those groups so they don't leak. For ungraceful exits (process crash, OOM kill), the next process to subscribe with broadcast on the same hostname scans ",[185,1071,1072],{},"XINFO GROUPS"," and destroys any matching ",[185,1075,1076],{},"\u003Cbase>__\u003Cthishost>_\u003Cotherpid>"," groups - cleanup happens automatically on first restart.",[182,1079,1080],{},"Limitations: cleanup only matches same-hostname orphans. If a broadcast handler ran on host A, the host went away, and the same code starts on host B, the host-A groups remain orphan in Redis. Run a periodic cleanup script if your topology has dynamic hostnames.",[208,1082,1084],{"id":1083},"dispatch-modes","Dispatch modes",[182,1086,1087,1088,666,1092,1095,1096,1099],{},"Redis Streams supports both ",[190,1089,1091],{"href":1090},"\u002Fdocs\u002Fpackages\u002Fmessaging#dispatch-modes","dispatch modes",[185,1093,1094],{},"'batch'"," (default) and ",[185,1097,1098],{},"'sliding'",". The choice trades latency under variable handler runtimes against the number of Redis connections held open.",[1047,1101,1103],{"id":1102},"batch-default","Batch (default)",[182,1105,1106,1107,1109,1110,1113,1114,1117],{},"A single consumer loop reads up to ",[185,1108,1012],{}," messages per ",[185,1111,1112],{},"XREADGROUP COUNT=concurrency"," call, runs them through ",[185,1115,1116],{},"Promise.allSettled",", then reads again. The slowest handler in a batch holds back the next read - this is the head-of-line behavior.",[215,1119,1122],{"className":1120,"code":1121,"language":987},[985],"subscription (1 connection)\n  └─ XREADGROUP COUNT=4 BLOCK 5000  ──► [m0, m1, m2, m3]  ──► allSettled\n                                                              │\n                                                              └──► next XREADGROUP\n",[185,1123,1121],{"__ignoreMap":221},[182,1125,1126,1127,300],{},"Best for high-throughput uniform workloads where per-message variance is small. One connection per subscription regardless of ",[185,1128,1012],{},[1047,1130,1132],{"id":1131},"sliding","Sliding",[182,1134,1135,1137,1138,1141,1142,1145],{},[185,1136,1012],{}," parallel lanes, each on its own duplicated Redis connection running ",[185,1139,1140],{},"XREADGROUP COUNT 1"," with a unique consumer name suffix ",[185,1143,1144],{},":laneN",". Redis distributes pending entries across the lanes (since they all join the same group), so each lane only ever holds one in-flight message. A slow message on lane 0 does not block lanes 1-N.",[215,1147,1150],{"className":1148,"code":1149,"language":987},[985],"subscription (4 connections, mode='sliding', concurrency=4)\n  ├─ XREADGROUP COUNT=1 (consumer :lane0)  ──► m0  ──► next read\n  ├─ XREADGROUP COUNT=1 (consumer :lane1)  ──► m1  ──► next read\n  ├─ XREADGROUP COUNT=1 (consumer :lane2)  ──► m2  ──► next read\n  └─ XREADGROUP COUNT=1 (consumer :lane3)  ──► m3  ──► next read\n",[185,1151,1149],{"__ignoreMap":221},[182,1153,1154],{},"Best when handler runtimes vary and you cannot tolerate one slow message stalling the others. Mirrors the BullMQ worker pattern.",[1047,1156,1158],{"id":1157},"picking-a-mode","Picking a mode",[680,1160,1161,1172],{},[683,1162,1163],{},[686,1164,1165,1167,1170],{},[689,1166],{},[689,1168,1169],{},"Batch",[689,1171,1132],{},[702,1173,1174,1186,1197],{},[686,1175,1176,1179,1182],{},[707,1177,1178],{},"Connection budget per subscription",[707,1180,1181],{},"1",[707,1183,1184],{},[185,1185,1012],{},[686,1187,1188,1191,1194],{},[707,1189,1190],{},"Behavior under runtime skew",[707,1192,1193],{},"head-of-line blocking",[707,1195,1196],{},"independent lanes",[686,1198,1199,1202,1205],{},[707,1200,1201],{},"Best fit",[707,1203,1204],{},"uniform workloads, high message volume",[707,1206,1207],{},"mixed workloads, latency-sensitive paths",[215,1209,1211],{"className":307,"code":1210,"language":309,"meta":221,"style":221},"MessagingModule.configure({\n  transport: redisStreamsTransport({ url: process.env.REDIS_URL! }),\n  dispatch: { mode: 'sliding', concurrency: 4 }, \u002F\u002F bus default\n})\n\n@On('order.placed', { mode: 'sliding', concurrency: 8 }) \u002F\u002F per-handler override\nasync onOrder(order: Order) { ... }\n",[185,1212,1213,1225,1264,1302,1308,1312,1359],{"__ignoreMap":221},[225,1214,1215,1217,1219,1221,1223],{"class":227,"line":228},[225,1216,556],{"class":324},[225,1218,300],{"class":320},[225,1220,427],{"class":396},[225,1222,400],{"class":324},[225,1224,403],{"class":320},[225,1226,1227,1229,1231,1233,1235,1238,1241,1243,1246,1248,1251,1253,1255,1258,1260,1262],{"class":227,"line":343},[225,1228,583],{"class":409},[225,1230,413],{"class":320},[225,1232,370],{"class":396},[225,1234,400],{"class":324},[225,1236,1237],{"class":320},"{",[225,1239,1240],{"class":409}," url",[225,1242,413],{"class":320},[225,1244,1245],{"class":324}," process",[225,1247,300],{"class":320},[225,1249,1250],{"class":324},"env",[225,1252,300],{"class":320},[225,1254,616],{"class":324},[225,1256,1257],{"class":320},"!",[225,1259,328],{"class":320},[225,1261,473],{"class":324},[225,1263,464],{"class":320},[225,1265,1266,1269,1271,1273,1276,1278,1280,1282,1284,1287,1290,1292,1295,1298],{"class":227,"line":363},[225,1267,1268],{"class":409},"  dispatch",[225,1270,413],{"class":320},[225,1272,321],{"class":320},[225,1274,1275],{"class":409}," mode",[225,1277,413],{"class":320},[225,1279,334],{"class":320},[225,1281,1131],{"class":234},[225,1283,461],{"class":320},[225,1285,1286],{"class":320},",",[225,1288,1289],{"class":409}," concurrency",[225,1291,413],{"class":320},[225,1293,1294],{"class":639}," 4",[225,1296,1297],{"class":320}," },",[225,1299,1301],{"class":1300},"sHwdD"," \u002F\u002F bus default\n",[225,1303,1304,1306],{"class":227,"line":383},[225,1305,499],{"class":320},[225,1307,502],{"class":324},[225,1309,1310],{"class":227,"line":390},[225,1311,387],{"emptyLinePlaceholder":386},[225,1313,1314,1316,1319,1321,1323,1326,1328,1330,1332,1334,1336,1338,1340,1342,1344,1346,1348,1351,1353,1356],{"class":227,"line":406},[225,1315,393],{"class":320},[225,1317,1318],{"class":396},"On",[225,1320,400],{"class":324},[225,1322,461],{"class":320},[225,1324,1325],{"class":234},"order.placed",[225,1327,461],{"class":320},[225,1329,1286],{"class":320},[225,1331,321],{"class":320},[225,1333,1275],{"class":409},[225,1335,413],{"class":320},[225,1337,334],{"class":320},[225,1339,1131],{"class":234},[225,1341,461],{"class":320},[225,1343,1286],{"class":320},[225,1345,1289],{"class":409},[225,1347,413],{"class":320},[225,1349,1350],{"class":639}," 8",[225,1352,328],{"class":320},[225,1354,1355],{"class":324},") ",[225,1357,1358],{"class":1300},"\u002F\u002F per-handler override\n",[225,1360,1361,1364,1367,1370,1372,1375],{"class":227,"line":419},[225,1362,1363],{"class":324},"async ",[225,1365,1366],{"class":396},"onOrder",[225,1368,1369],{"class":324},"(order: Order) ",[225,1371,1237],{"class":320},[225,1373,1374],{"class":320}," ...",[225,1376,1377],{"class":320}," }\n",[182,1379,1380,1381,1384,1385,781,1388,1391],{},"Lane consumer names show up in ",[185,1382,1383],{},"XINFO CONSUMERS \u003Cstream> \u003Cgroup>"," as ",[185,1386,1387],{},"${baseConsumer}:lane0",[185,1389,1390],{},"${baseConsumer}:lane1"," etc, which makes lane-level diagnostics straightforward.",[1393,1394,1395],"tip",{},[182,1396,1397,1398,1400,1401,1404,1405,1407],{},"Lane count equals ",[185,1399,1012],{}," in sliding mode - size it for the ",[742,1402,1403],{},"expected parallelism within a single replica",", not for total cluster throughput. Add replicas to scale horizontally; raise sliding ",[185,1406,1012],{}," to soak up runtime variance per replica.",[208,1409,1411],{"id":1410},"consumer-groups-and-deployment","Consumer groups and deployment",[182,1413,1414],{},"Consumer groups are how Redis Streams maps logical roles to running replicas. The deployment story is straightforward:",[1416,1417,1418,1440,1446],"ul",{},[1419,1420,1421,1424,1425,781,1428,781,1431,1434,1435,1439],"li",{},[742,1422,1423],{},"One consumer group per logical role."," ",[185,1426,1427],{},"email-workers",[185,1429,1430],{},"analytics",[185,1432,1433],{},"audit-log"," - pick names that describe the ",[1436,1437,1438],"em",{},"job",", not the host.",[1419,1441,1442,1445],{},[742,1443,1444],{},"Every replica of the same role joins the same group."," Redis automatically load-balances messages across consumers within a group, so adding replicas scales throughput linearly until a single message is no longer the bottleneck.",[1419,1447,1448,1451,1452,1455,1456,1459],{},[742,1449,1450],{},"Fan-out across groups is automatic."," Two ",[185,1453,1454],{},"@On('user.created', { group: 'email-workers' })"," handlers in one app + two ",[185,1457,1458],{},"@On('user.created', { group: 'analytics' })"," handlers in a different app = both groups receive every message, independently.",[182,1461,1462,1463,1466,1467,1470,1471,1474,1475,1477,1478,1482],{},"The most common misconfig is mixing up consumer names and group names. ",[742,1464,1465],{},"Group names are shared across replicas"," (that's what enables load balancing). ",[742,1468,1469],{},"Consumer names must be unique per replica"," - the transport defaults to ",[185,1472,1473],{},"hostname:pid:rand8"," which is safe by construction; only override ",[185,1476,917],{}," if you have a reason. See ",[190,1479,1481],{"href":1480},"\u002Fdocs\u002Fpackages\u002Fmessaging#groups-and-fan-out","Groups and fan-out"," on the Overview page for the full mental model.",[208,1484,1486],{"id":1485},"retry-mechanics","Retry mechanics",[182,1488,1489],{},"Understanding what happens in Redis when a handler nacks helps when debugging or operating the cluster.",[215,1491,1494],{"className":1492,"code":1493,"language":987},[985],"main stream ──► handler ──► ack ───► (done)\n                  │\n                  └─nack──► retry ZSET ──(delay)──► main stream\n                               │\n                               └─maxAttempts──► \u003Ctopic>.dlq\n",[185,1495,1493],{"__ignoreMap":221},[182,1497,1498],{},"Step by step:",[1500,1501,1502,1508,1514,1521,1537,1554],"ol",{},[1419,1503,1504,1507],{},[185,1505,1506],{},"XADD \u003Ctopic>"," writes the envelope as a stream entry.",[1419,1509,1510,1511,300],{},"A consumer reads via ",[185,1512,1513],{},"XREADGROUP GROUP \u003Cgroup> \u003Cconsumer> COUNT \u003Cn> BLOCK \u003Cms> STREAMS \u003Ctopic> >",[1419,1515,1516,1517,1520],{},"On ack: a single ",[185,1518,1519],{},"XACK \u003Ctopic> \u003Cgroup> \u003Cid>"," removes the entry from the group's pending entries list (PEL).",[1419,1522,1523,1524,1424,1526,1424,1529,1532,1533,1536],{},"On nack: an atomic Lua script runs ",[185,1525,946],{},[1436,1527,1528],{},"and",[185,1530,1531],{},"ZADD \u003Ctopic>:retry \u003CretryAt> \u003Cnew envelope>"," in one server-side step. The new envelope has ",[185,1534,1535],{},"meta.attempt"," incremented; the ZSET score is the absolute epoch ms when the retry becomes due.",[1419,1538,1539,1540,1542,1543,1546,1547,1549,1550,1553],{},"Every ",[185,1541,798],{},", a background scheduler runs another atomic Lua script: ",[185,1544,1545],{},"ZRANGEBYSCORE \u003Ctopic>:retry -inf \u003Cnow>"," finds due entries, then for each: ",[185,1548,1506],{}," republishes it and ",[185,1551,1552],{},"ZREM"," removes it from the ZSET.",[1419,1555,1556,1557,1559,1560,1562,1563,1566,1567,1570],{},"After ",[185,1558,780],{},", instead of going back to the retry ZSET, a third atomic Lua script runs ",[185,1561,946],{}," on the original entry plus ",[185,1564,1565],{},"XADD \u003Ctopic>.dlq"," with ",[185,1568,1569],{},"meta.lastError"," populated.",[182,1572,1573,1574,1577,1578,1581,1582,1584,1585,1588],{},"All three transitions use Lua scripts (registered via ioredis ",[185,1575,1576],{},"defineCommand"," and cached server-side via EVALSHA) because each one couples two Redis operations that ",[1436,1579,1580],{},"must"," succeed or fail together. Without atomicity, a process crash between, say, ",[185,1583,946],{}," and ",[185,1586,1587],{},"ZADD"," would leave the message acknowledged but never re-scheduled - silently lost.",[208,1590,1592],{"id":1591},"idle-reclaim","Idle reclaim",[182,1594,1595],{},"Consumer groups track in-flight messages in the pending entries list (PEL). If a consumer dies while processing - process crash, container OOM, network partition - its pending entries stay in the PEL forever unless something reclaims them.",[182,1597,1539,1598,1600,1601,1604,1605,1607],{},[185,1599,818],{}," (default 30s), the transport runs ",[185,1602,1603],{},"XAUTOCLAIM \u003Ctopic> \u003Cgroup> \u003Cmy-consumer> \u003CminIdleMs>"," on each active subscription. Any pending entry that has been idle for at least ",[185,1606,841],{}," (default 60s) is transferred to the current consumer, which then processes it as if it were a fresh delivery. The retry counter and DLQ logic apply normally.",[182,1609,1610,1611,1614],{},"Worst-case recovery time after a consumer crash is ",[185,1612,1613],{},"minIdleMs + reclaimIntervalMs"," - about 90 seconds with the defaults. Shorten both for latency-sensitive workloads; lengthen them when reducing Redis traffic matters more than reaction speed.",[208,1616,910],{"id":1617},"graceful-drain",[182,1619,1620,1622,1623,1625,1626,1628,1629,1631],{},[185,1621,905],{}," waits up to ",[185,1624,891],{}," for in-flight handlers to finish before closing the Redis client. Without drain, a SIGTERM mid-handler aborts the consumer loop and quits the client while a handler is still running - the message stays un-acked and reappears via ",[185,1627,833],{}," after ",[185,1630,841],{},", producing a duplicate on the next consumer.",[182,1633,1634,1635,1638,1639,1642,1643,1645,1646,1649,1650,1653,1654,1656,1657,1660],{},"Drain order: ",[185,1636,1637],{},"abort each subscription"," → ",[185,1640,1641],{},"disconnect each subClient"," (forces in-flight ",[185,1644,876],{}," to throw ",[185,1647,1648],{},"Connection is closed",", the consumer loop sees the abort flag and exits cleanly) → ",[185,1651,1652],{},"await waitForDrain"," so handlers finish their final ",[185,1655,946],{}," \u002F Lua calls through the still-alive pubClient → ",[185,1658,1659],{},"quit pubClient"," (only when the transport owns it).",[1393,1662,1663],{},[182,1664,1665,1666,1668,1669,1671,1672,1674,1675,1678],{},"Because subClients are forcibly disconnected on abort, shutdown is bounded by ",[185,1667,891],{}," regardless of ",[185,1670,862],{},". Size ",[185,1673,891],{}," to cover expected handler runtime; the previous \"",[185,1676,1677],{},"drainTimeoutMs >= blockMs + handler runtime","\" rule no longer applies.",[182,1680,1681,1682,1684,1685,1688,1689,1692],{},"DLQ writes during drain are safe - the ",[185,1683,957],{}," Lua script is awaited synchronously inside the tracked ",[185,1686,1687],{},"processMessage"," promise, so the XADD to ",[185,1690,1691],{},"\u003Ctopic>.dlq"," completes before the drain promise resolves.",[208,1694,1016],{"id":1695},"redisidempotencystore",[182,1697,1698,1699,1705,1706,1708],{},"Production-ready ",[190,1700,1702],{"href":1701},"\u002Fdocs\u002Fpackages\u002Fmessaging#idempotency",[185,1703,1704],{},"IdempotencyStore"," backed by Redis. Pair with any ",[185,1707,198],{}," (Redis Streams, NATS, in-memory, etc.) - the store is transport-agnostic.",[215,1710,1712],{"className":307,"code":1711,"language":309,"meta":221,"style":221},"import { Module } from '@miiajs\u002Fcore'\nimport { MessagingModule } from '@miiajs\u002Fmessaging'\nimport { redisIdempotencyStore, redisStreamsTransport } from '@miiajs\u002Fmessaging-redis'\n\n@Module({\n  imports: [\n    MessagingModule.configure({\n      transport: redisStreamsTransport({ url: 'redis:\u002F\u002Flocalhost:6379' }),\n      idempotency: redisIdempotencyStore({\n        url: 'redis:\u002F\u002Flocalhost:6379',\n        keyPrefix: 'orders-svc:idem:',\n      }),\n    }),\n  ],\n})\nclass AppModule {}\n",[185,1713,1714,1732,1750,1773,1777,1787,1795,1807,1835,1848,1862,1878,1886,1894,1900,1907],{"__ignoreMap":221},[225,1715,1716,1718,1720,1722,1724,1726,1728,1730],{"class":227,"line":228},[225,1717,317],{"class":316},[225,1719,321],{"class":320},[225,1721,325],{"class":324},[225,1723,328],{"class":320},[225,1725,331],{"class":316},[225,1727,334],{"class":320},[225,1729,337],{"class":234},[225,1731,340],{"class":320},[225,1733,1734,1736,1738,1740,1742,1744,1746,1748],{"class":227,"line":343},[225,1735,317],{"class":316},[225,1737,321],{"class":320},[225,1739,350],{"class":324},[225,1741,328],{"class":320},[225,1743,331],{"class":316},[225,1745,334],{"class":320},[225,1747,194],{"class":234},[225,1749,340],{"class":320},[225,1751,1752,1754,1756,1759,1761,1763,1765,1767,1769,1771],{"class":227,"line":363},[225,1753,317],{"class":316},[225,1755,321],{"class":320},[225,1757,1758],{"class":324}," redisIdempotencyStore",[225,1760,1286],{"class":320},[225,1762,370],{"class":324},[225,1764,328],{"class":320},[225,1766,331],{"class":316},[225,1768,334],{"class":320},[225,1770,187],{"class":234},[225,1772,340],{"class":320},[225,1774,1775],{"class":227,"line":383},[225,1776,387],{"emptyLinePlaceholder":386},[225,1778,1779,1781,1783,1785],{"class":227,"line":390},[225,1780,393],{"class":320},[225,1782,397],{"class":396},[225,1784,400],{"class":324},[225,1786,403],{"class":320},[225,1788,1789,1791,1793],{"class":227,"line":406},[225,1790,410],{"class":409},[225,1792,413],{"class":320},[225,1794,416],{"class":324},[225,1796,1797,1799,1801,1803,1805],{"class":227,"line":419},[225,1798,422],{"class":324},[225,1800,300],{"class":320},[225,1802,427],{"class":396},[225,1804,400],{"class":324},[225,1806,403],{"class":320},[225,1808,1809,1811,1813,1815,1817,1819,1821,1823,1825,1827,1829,1831,1833],{"class":227,"line":434},[225,1810,437],{"class":409},[225,1812,413],{"class":320},[225,1814,370],{"class":396},[225,1816,400],{"class":324},[225,1818,1237],{"class":320},[225,1820,1240],{"class":409},[225,1822,413],{"class":320},[225,1824,334],{"class":320},[225,1826,458],{"class":234},[225,1828,461],{"class":320},[225,1830,328],{"class":320},[225,1832,473],{"class":324},[225,1834,464],{"class":320},[225,1836,1837,1840,1842,1844,1846],{"class":227,"line":448},[225,1838,1839],{"class":409},"      idempotency",[225,1841,413],{"class":320},[225,1843,1758],{"class":396},[225,1845,400],{"class":324},[225,1847,403],{"class":320},[225,1849,1850,1852,1854,1856,1858,1860],{"class":227,"line":467},[225,1851,451],{"class":409},[225,1853,413],{"class":320},[225,1855,334],{"class":320},[225,1857,458],{"class":234},[225,1859,461],{"class":320},[225,1861,464],{"class":320},[225,1863,1864,1867,1869,1871,1874,1876],{"class":227,"line":478},[225,1865,1866],{"class":409},"        keyPrefix",[225,1868,413],{"class":320},[225,1870,334],{"class":320},[225,1872,1873],{"class":234},"orders-svc:idem:",[225,1875,461],{"class":320},[225,1877,464],{"class":320},[225,1879,1880,1882,1884],{"class":227,"line":488},[225,1881,470],{"class":320},[225,1883,473],{"class":324},[225,1885,464],{"class":320},[225,1887,1888,1890,1892],{"class":227,"line":496},[225,1889,481],{"class":320},[225,1891,473],{"class":324},[225,1893,464],{"class":320},[225,1895,1896,1898],{"class":227,"line":505},[225,1897,491],{"class":324},[225,1899,464],{"class":320},[225,1901,1903,1905],{"class":227,"line":1902},15,[225,1904,499],{"class":320},[225,1906,502],{"class":324},[225,1908,1910,1912,1914],{"class":227,"line":1909},16,[225,1911,509],{"class":508},[225,1913,512],{"class":231},[225,1915,515],{"class":320},[182,1917,1918,1921,1922,1925,1926,1929,1930,1921,1933,1936,1937,300],{},[185,1919,1920],{},"claim()"," is ",[185,1923,1924],{},"SET \u003Cprefix>\u003Cid> 1 NX EX \u003Cttl>"," - atomic, so concurrent claims for the same id produce exactly one ",[185,1927,1928],{},"true",". ",[185,1931,1932],{},"release()",[185,1934,1935],{},"DEL \u003Cprefix>\u003Cid>",". Keys auto-expire after the TTL passed to ",[185,1938,1939],{},"@Idempotent",[680,1941,1942,1954],{},[683,1943,1944],{},[686,1945,1946,1948,1950,1952],{},[689,1947,691],{},[689,1949,694],{},[689,1951,697],{},[689,1953,700],{},[702,1955,1956,1972,1994],{},[686,1957,1958,1962,1966,1968],{},[707,1959,1960],{},[185,1961,711],{},[707,1963,1964],{},[185,1965,716],{},[707,1967,719],{},[707,1969,722,1970,300],{},[185,1971,725],{},[686,1973,1974,1978,1982,1984],{},[707,1975,1976],{},[185,1977,725],{},[707,1979,1980],{},[185,1981,105],{},[707,1983,719],{},[707,1985,1986,1987,745,1989,749,1991,1993],{},"Pre-built ioredis instance. The store does ",[742,1988,744],{},[185,1990,748],{},[185,1992,752],{}," on a user-supplied client.",[686,1995,1996,2001,2005,2010],{},[707,1997,1998],{},[185,1999,2000],{},"keyPrefix",[707,2002,2003],{},[185,2004,716],{},[707,2006,2007],{},[185,2008,2009],{},"'miia:idem:'",[707,2011,2012,2013,2016],{},"Key prefix in Redis. ",[742,2014,2015],{},"Set a per-service prefix"," when multiple services share a Redis instance so claims do not collide across services.",[1393,2018,2019],{},[182,2020,2021,2022,2025,2026,2029,2030,2033,2034,2037,2038,2040,2041,2044],{},"The Redis client used for idempotency can be the ",[742,2023,2024],{},"same"," ioredis instance you pass to ",[185,2027,2028],{},"redisStreamsTransport({ client })",". Multiplexing one connection is fine - the Redis protocol is request\u002Fresponse and ",[185,2031,2032],{},"claim","\u002F",[185,2035,2036],{},"release"," calls are short and non-blocking. The transport never blocks on this connection: blocking ",[185,2039,980],{}," runs on isolated ",[190,2042,2043],{"href":883},"per-subscribe duplicates",", so sharing the parent client with the idempotency store (or any other subsystem) does not create publish-latency contention.",[208,2046,757],{"id":2047},"pre-built-ioredis-client",[182,2049,2050,2051,2053],{},"When you want to share a connection with other parts of your app - caching, rate limiting, session storage - pass a pre-built ",[185,2052,105],{}," instance instead of a URL:",[215,2055,2057],{"className":307,"code":2056,"language":309,"meta":221,"style":221},"import { Redis } from 'ioredis'\nimport { redisStreamsTransport } from '@miiajs\u002Fmessaging-redis'\n\nconst client = new Redis(process.env.REDIS_URL!)\n\nMessagingModule.configure({\n  transport: redisStreamsTransport({ client }),\n})\n",[185,2058,2059,2078,2096,2100,2131,2135,2147,2167],{"__ignoreMap":221},[225,2060,2061,2063,2065,2068,2070,2072,2074,2076],{"class":227,"line":228},[225,2062,317],{"class":316},[225,2064,321],{"class":320},[225,2066,2067],{"class":324}," Redis",[225,2069,328],{"class":320},[225,2071,331],{"class":316},[225,2073,334],{"class":320},[225,2075,295],{"class":234},[225,2077,340],{"class":320},[225,2079,2080,2082,2084,2086,2088,2090,2092,2094],{"class":227,"line":343},[225,2081,317],{"class":316},[225,2083,321],{"class":320},[225,2085,370],{"class":324},[225,2087,328],{"class":320},[225,2089,331],{"class":316},[225,2091,334],{"class":320},[225,2093,187],{"class":234},[225,2095,340],{"class":320},[225,2097,2098],{"class":227,"line":363},[225,2099,387],{"emptyLinePlaceholder":386},[225,2101,2102,2105,2108,2111,2114,2116,2119,2121,2123,2125,2127,2129],{"class":227,"line":383},[225,2103,2104],{"class":508},"const",[225,2106,2107],{"class":324}," client ",[225,2109,2110],{"class":320},"=",[225,2112,2113],{"class":320}," new",[225,2115,2067],{"class":396},[225,2117,2118],{"class":324},"(process",[225,2120,300],{"class":320},[225,2122,1250],{"class":324},[225,2124,300],{"class":320},[225,2126,616],{"class":324},[225,2128,1257],{"class":320},[225,2130,502],{"class":324},[225,2132,2133],{"class":227,"line":390},[225,2134,387],{"emptyLinePlaceholder":386},[225,2136,2137,2139,2141,2143,2145],{"class":227,"line":406},[225,2138,556],{"class":324},[225,2140,300],{"class":320},[225,2142,427],{"class":396},[225,2144,400],{"class":324},[225,2146,403],{"class":320},[225,2148,2149,2151,2153,2155,2157,2159,2161,2163,2165],{"class":227,"line":419},[225,2150,583],{"class":409},[225,2152,413],{"class":320},[225,2154,370],{"class":396},[225,2156,400],{"class":324},[225,2158,1237],{"class":320},[225,2160,2107],{"class":324},[225,2162,499],{"class":320},[225,2164,473],{"class":324},[225,2166,464],{"class":320},[225,2168,2169,2171],{"class":227,"line":434},[225,2170,499],{"class":320},[225,2172,502],{"class":324},[182,2174,2175,2176,2178],{},"The transport tracks an ",[185,2177,1020],{}," flag internally:",[1416,2180,2181,2201],{},[1419,2182,2183,2188,2189,2191,2192,2195,2196,2191,2198,2200],{},[742,2184,2185,2187],{},[185,2186,711],{}," form:"," the transport creates the ioredis instance, calls ",[185,2190,748],{}," in ",[185,2193,2194],{},"onInit",", and ",[185,2197,752],{},[185,2199,1064],{},". It owns the lifecycle of the parent client.",[1419,2202,2203,2207,2208,749,2210,2212,2213,2216,2217,2220],{},[742,2204,2205,2187],{},[185,2206,725],{}," the transport never calls ",[185,2209,748],{},[185,2211,752],{}," on your client. ",[742,2214,2215],{},"Your code owns the lifecycle."," Initialise the connection before the app starts, and tear it down after ",[185,2218,2219],{},"app.destroy()"," returns.",[182,2222,2223,2224,2227,2228,2230,2231,2233,2234,2236,2237,2239],{},"In both cases, the transport calls ",[185,2225,2226],{},"client.duplicate({ lazyConnect: true })"," for every ",[185,2229,968],{}," to spin up a dedicated blocking client (see ",[190,2232,933],{"href":883},"). Those duplicates are owned by the transport regardless of ",[185,2235,1020],{}," - they are created and closed by us, never by your code. The ",[185,2238,1020],{}," flag only governs the parent.",[182,2241,2242],{},"This split exists so you can multiplex the parent connection across multiple subsystems (idempotency store, cache, rate limiter) without the transport reaching in and quitting it out from under you, while still getting per-subscribe blocking isolation for free.",[208,2244,33],{"id":2245},"testing",[182,2247,2248,2249,2252,2253,2256],{},"Integration tests need a real Redis - ",[185,2250,2251],{},"ioredis-mock"," and similar libraries do not implement Streams faithfully enough for retry\u002FDLQ semantics. The pattern used by the package's own test suite is to skip when ",[185,2254,2255],{},"REDIS_TEST_URL"," is unset:",[215,2258,2260],{"className":307,"code":2259,"language":309,"meta":221,"style":221},"import { describe, it, expect } from 'bun:test'\nimport { Redis } from 'ioredis'\nimport { RedisStreamsTransport } from '@miiajs\u002Fmessaging-redis'\nimport { randomUUID } from 'node:crypto'\n\nconst REDIS_URL = process.env.REDIS_TEST_URL\nconst d = REDIS_URL ? describe : describe.skip\n\nd('my events integration', () => {\n  it('delivers across consumers', async () => {\n    const transport = new RedisStreamsTransport({\n      url: REDIS_URL!,\n      retry: { backoffMs: 50 },\n      blockMs: 200,\n    })\n    await transport.onInit()\n    \u002F\u002F ... use transport ...\n    await transport.onDestroy()\n  })\n})\n",[185,2261,2262,2292,2310,2329,2349,2353,2373,2399,2403,2427,2452,2471,2484,2503,2515,2521,2535,2541,2554,2561],{"__ignoreMap":221},[225,2263,2264,2266,2268,2271,2273,2276,2278,2281,2283,2285,2287,2290],{"class":227,"line":228},[225,2265,317],{"class":316},[225,2267,321],{"class":320},[225,2269,2270],{"class":324}," describe",[225,2272,1286],{"class":320},[225,2274,2275],{"class":324}," it",[225,2277,1286],{"class":320},[225,2279,2280],{"class":324}," expect",[225,2282,328],{"class":320},[225,2284,331],{"class":316},[225,2286,334],{"class":320},[225,2288,2289],{"class":234},"bun:test",[225,2291,340],{"class":320},[225,2293,2294,2296,2298,2300,2302,2304,2306,2308],{"class":227,"line":343},[225,2295,317],{"class":316},[225,2297,321],{"class":320},[225,2299,2067],{"class":324},[225,2301,328],{"class":320},[225,2303,331],{"class":316},[225,2305,334],{"class":320},[225,2307,295],{"class":234},[225,2309,340],{"class":320},[225,2311,2312,2314,2316,2319,2321,2323,2325,2327],{"class":227,"line":363},[225,2313,317],{"class":316},[225,2315,321],{"class":320},[225,2317,2318],{"class":324}," RedisStreamsTransport",[225,2320,328],{"class":320},[225,2322,331],{"class":316},[225,2324,334],{"class":320},[225,2326,187],{"class":234},[225,2328,340],{"class":320},[225,2330,2331,2333,2335,2338,2340,2342,2344,2347],{"class":227,"line":383},[225,2332,317],{"class":316},[225,2334,321],{"class":320},[225,2336,2337],{"class":324}," randomUUID",[225,2339,328],{"class":320},[225,2341,331],{"class":316},[225,2343,334],{"class":320},[225,2345,2346],{"class":234},"node:crypto",[225,2348,340],{"class":320},[225,2350,2351],{"class":227,"line":390},[225,2352,387],{"emptyLinePlaceholder":386},[225,2354,2355,2357,2360,2362,2364,2366,2368,2370],{"class":227,"line":406},[225,2356,2104],{"class":508},[225,2358,2359],{"class":324}," REDIS_URL ",[225,2361,2110],{"class":320},[225,2363,1245],{"class":324},[225,2365,300],{"class":320},[225,2367,1250],{"class":324},[225,2369,300],{"class":320},[225,2371,2372],{"class":324},"REDIS_TEST_URL\n",[225,2374,2375,2377,2380,2382,2384,2387,2390,2392,2394,2396],{"class":227,"line":419},[225,2376,2104],{"class":508},[225,2378,2379],{"class":324}," d ",[225,2381,2110],{"class":320},[225,2383,2359],{"class":324},[225,2385,2386],{"class":320},"?",[225,2388,2389],{"class":324}," describe ",[225,2391,413],{"class":320},[225,2393,2270],{"class":324},[225,2395,300],{"class":320},[225,2397,2398],{"class":324},"skip\n",[225,2400,2401],{"class":227,"line":434},[225,2402,387],{"emptyLinePlaceholder":386},[225,2404,2405,2408,2410,2412,2415,2417,2419,2422,2424],{"class":227,"line":448},[225,2406,2407],{"class":396},"d",[225,2409,400],{"class":324},[225,2411,461],{"class":320},[225,2413,2414],{"class":234},"my events integration",[225,2416,461],{"class":320},[225,2418,1286],{"class":320},[225,2420,2421],{"class":320}," ()",[225,2423,573],{"class":508},[225,2425,2426],{"class":320}," {\n",[225,2428,2429,2432,2434,2436,2439,2441,2443,2446,2448,2450],{"class":227,"line":467},[225,2430,2431],{"class":396},"  it",[225,2433,400],{"class":409},[225,2435,461],{"class":320},[225,2437,2438],{"class":234},"delivers across consumers",[225,2440,461],{"class":320},[225,2442,1286],{"class":320},[225,2444,2445],{"class":508}," async",[225,2447,2421],{"class":320},[225,2449,573],{"class":508},[225,2451,2426],{"class":320},[225,2453,2454,2457,2460,2463,2465,2467,2469],{"class":227,"line":478},[225,2455,2456],{"class":508},"    const",[225,2458,2459],{"class":324}," transport",[225,2461,2462],{"class":320}," =",[225,2464,2113],{"class":320},[225,2466,2318],{"class":396},[225,2468,400],{"class":409},[225,2470,403],{"class":320},[225,2472,2473,2476,2478,2481],{"class":227,"line":488},[225,2474,2475],{"class":409},"      url",[225,2477,413],{"class":320},[225,2479,2480],{"class":324}," REDIS_URL",[225,2482,2483],{"class":320},"!,\n",[225,2485,2486,2489,2491,2493,2496,2498,2501],{"class":227,"line":496},[225,2487,2488],{"class":409},"      retry",[225,2490,413],{"class":320},[225,2492,321],{"class":320},[225,2494,2495],{"class":409}," backoffMs",[225,2497,413],{"class":320},[225,2499,2500],{"class":639}," 50",[225,2502,643],{"class":320},[225,2504,2505,2508,2510,2513],{"class":227,"line":505},[225,2506,2507],{"class":409},"      blockMs",[225,2509,413],{"class":320},[225,2511,2512],{"class":639}," 200",[225,2514,464],{"class":320},[225,2516,2517,2519],{"class":227,"line":1902},[225,2518,481],{"class":320},[225,2520,502],{"class":409},[225,2522,2523,2526,2528,2530,2532],{"class":227,"line":1909},[225,2524,2525],{"class":316},"    await",[225,2527,2459],{"class":324},[225,2529,300],{"class":320},[225,2531,2194],{"class":396},[225,2533,2534],{"class":409},"()\n",[225,2536,2538],{"class":227,"line":2537},17,[225,2539,2540],{"class":1300},"    \u002F\u002F ... use transport ...\n",[225,2542,2544,2546,2548,2550,2552],{"class":227,"line":2543},18,[225,2545,2525],{"class":316},[225,2547,2459],{"class":324},[225,2549,300],{"class":320},[225,2551,1064],{"class":396},[225,2553,2534],{"class":409},[225,2555,2557,2559],{"class":227,"line":2556},19,[225,2558,648],{"class":320},[225,2560,502],{"class":409},[225,2562,2564,2566],{"class":227,"line":2563},20,[225,2565,499],{"class":320},[225,2567,502],{"class":324},[182,2569,2570],{},"Two operational tips:",[1416,2572,2573,2579],{},[1419,2574,2575,2578],{},[742,2576,2577],{},"Always use unique topic names per test"," (UUID suffix is enough). Cross-test pollution from a leftover stream is the #1 source of flaky retry\u002FDLQ assertions.",[1419,2580,2581,2587,2588,1584,2591,2593,2594,2596],{},[742,2582,2583,2584,2586],{},"Lower ",[185,2585,862],{}," to 200ms"," in tests so ",[185,2589,2590],{},"unsubscribe()",[185,2592,905],{}," return promptly instead of waiting for the default 5s ",[185,2595,876],{}," to time out.",[182,2598,2599,2600,2602],{},"Provisioning the Redis instance itself - local container, managed service, in-cluster pod - is up to you; the transport only needs a reachable URL exported as ",[185,2601,2255],{}," before the suite runs.",[208,2604,2606],{"id":2605},"exports","Exports",[215,2608,2610],{"className":307,"code":2609,"language":309,"meta":221,"style":221},"import {\n  RedisIdempotencyStore,\n  redisIdempotencyStore,\n  RedisStreamsTransport,\n  redisStreamsTransport,\n  type RedisIdempotencyStoreOptions,\n  type RedisStreamsTransportOptions,\n} from '@miiajs\u002Fmessaging-redis'\n",[185,2611,2612,2618,2625,2632,2639,2646,2656,2665],{"__ignoreMap":221},[225,2613,2614,2616],{"class":227,"line":228},[225,2615,317],{"class":316},[225,2617,2426],{"class":320},[225,2619,2620,2623],{"class":227,"line":343},[225,2621,2622],{"class":324},"  RedisIdempotencyStore",[225,2624,464],{"class":320},[225,2626,2627,2630],{"class":227,"line":363},[225,2628,2629],{"class":324},"  redisIdempotencyStore",[225,2631,464],{"class":320},[225,2633,2634,2637],{"class":227,"line":383},[225,2635,2636],{"class":324},"  RedisStreamsTransport",[225,2638,464],{"class":320},[225,2640,2641,2644],{"class":227,"line":390},[225,2642,2643],{"class":324},"  redisStreamsTransport",[225,2645,464],{"class":320},[225,2647,2648,2651,2654],{"class":227,"line":406},[225,2649,2650],{"class":316},"  type",[225,2652,2653],{"class":324}," RedisIdempotencyStoreOptions",[225,2655,464],{"class":320},[225,2657,2658,2660,2663],{"class":227,"line":419},[225,2659,2650],{"class":316},[225,2661,2662],{"class":324}," RedisStreamsTransportOptions",[225,2664,464],{"class":320},[225,2666,2667,2669,2671,2673,2675],{"class":227,"line":434},[225,2668,499],{"class":320},[225,2670,331],{"class":316},[225,2672,334],{"class":320},[225,2674,187],{"class":234},[225,2676,340],{"class":320},[2678,2679,2680],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}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 .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 .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}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 .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}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":221,"searchDepth":343,"depth":343,"links":2682},[2683,2684,2685,2686,2689,2694,2695,2696,2697,2698,2699,2700,2701],{"id":210,"depth":343,"text":13},{"id":303,"depth":343,"text":304},{"id":677,"depth":343,"text":678},{"id":932,"depth":343,"text":933,"children":2687},[2688],{"id":1049,"depth":363,"text":1050},{"id":1083,"depth":343,"text":1084,"children":2690},[2691,2692,2693],{"id":1102,"depth":363,"text":1103},{"id":1131,"depth":363,"text":1132},{"id":1157,"depth":363,"text":1158},{"id":1410,"depth":343,"text":1411},{"id":1485,"depth":343,"text":1486},{"id":1591,"depth":343,"text":1592},{"id":1617,"depth":343,"text":910},{"id":1695,"depth":343,"text":1016},{"id":2047,"depth":343,"text":757},{"id":2245,"depth":343,"text":33},{"id":2605,"depth":343,"text":2606},"Redis Streams transport for @miiajs\u002Fmessaging - consumer groups, ZSET-backed retry, auto-DLQ.","md",{},{"title":105,"description":2702},"jeC2YVGAHN1Kp04KhG9zGfEWk0WEz8nTYCwX7XJP8SU",[2708,2710],{"title":27,"path":99,"stem":100,"description":2709,"children":-1},"Decorator-driven event bus with exponential backoff retry, auto-DLQ, and pluggable transports.",{"title":109,"path":110,"stem":111,"description":2711,"children":-1},"Static file server with Range, conditional GET, charset, dotfile guard, and SPA fallback.",1778575276625]