[{"data":1,"prerenderedAt":4779},["ShallowReactive",2],{"/projects/distkit":3,"/projects/distkit--related":2468},{"id":4,"title":5,"body":6,"date":2453,"description":2454,"extension":2455,"image_url":2456,"link":47,"meta":2457,"navigation":383,"path":2459,"seo":2460,"stem":2461,"tags":2462,"__hash__":2467},"projects/projects/distkit.md","distkit",{"type":7,"value":8,"toc":2401},"minimark",[9,15,18,27,32,36,87,91,96,103,106,110,116,122,281,284,288,295,330,689,692,695,707,861,864,868,871,901,904,907,916,1299,1302,1305,1317,1614,1617,1620,1626,1786,1789,1793,1812,1835,1861,1887,2091,2094,2098,2101,2105,2109,2113,2117,2121,2125,2151,2154,2157,2160,2164,2167,2170,2191,2193,2196,2199,2203,2206,2209,2235,2237,2240,2243,2246,2249,2252,2274,2276,2279,2282,2286,2289,2292,2312,2314,2317,2320,2324,2327,2330,2358,2360,2362,2397],[10,11],"content-time-detail",{"className":12,"date":14},[13],"mb-2","2026-04-03",[16,17,5],"h1",{"id":5},[19,20,21,22,26],"p",{},"Distkit (DISTributed systems KIT) is a Rust library of distributed systems primitives backed by Redis. It gives you four counter types (strict, lax, strict instance-aware, and lax instance-aware) for global consistency, it also includes sliding-window rate limiting via the ",[23,24,25],"code",{},"trypema",".",[28,29],"div",{"className":30},[31],"mb-6",[33,34],"tag-list",{":tags":35},"[\"Rust\", \"Redis\", \"Distributed Systems\", \"Library\", \"Async\"]",[28,37,44,64,77],{"className":38},[39,40,41,42,43,31],"flex","flex-wrap","items-center","gap-3","mt-4",[45,46,57,60],"a",{"href":47,"rel":48,"className":50,"target":56},"https://github.com/dev-davexoyinbo/distkit",[49],"nofollow",[51,52,53,54,55],"button","button-xs","xs:button-sm","sm:button-base","button-amberwood","\\_blank",[19,58,59],{},"GitHub Repo",[61,62],"content-icon",{"icon":63},"mdi:external-link",[45,65,72,75],{"href":66,"rel":67,"className":68,"target":56},"https://crates.io/crates/distkit",[49],[51,52,53,54,69,70,71],"button-plain","border","border-charcoal-800",[19,73,74],{},"crates.io",[61,76],{"icon":63},[45,78,82,85],{"href":79,"rel":80,"className":81,"target":56},"https://docs.rs/distkit",[49],[51,52,53,54,69,70,71],[19,83,84],{},"API Docs (docs.rs)",[61,86],{"icon":63},[28,88],{"className":89},[90],"mb-10",[92,93,95],"h2",{"id":94},"overview","Overview",[28,97,100],{"className":98},[99],"mb-14",[19,101,102],{},"Distkit ships four distributed counter implementations and re-exports a rate limiter (trypema).",[28,104],{"className":105},[90],[92,107,109],{"id":108},"counter-types","Counter types",[28,111,113],{"className":112},[31],[19,114,115],{},"The counter types are what they sound like. They are used to hold counters across a distributed system. The counters are in two categories, the normal counters (StrictCounter, LaxCounter) and the instance aware counters. The normal counters behave like normal counters and can be useful for counting events across a shared key or namespace, while an instance aware counter keeps track of the counter each instance (or server) holds, if the instance dies, then the count it holds would be subtracted from the cumulative total. If the server is marked as dead because of, say, a network interruption, when the instance comes back online the values are recovered and synchronized with the global cumulative.",[28,117,119],{"className":118},[31],[19,120,121],{},"There is also a concept of epochs for the instance aware counters. An epoch is more like the version number of era the counters are in. If the epoch changes, it means we have moved on to start from zero. During recovery, an instance might attempt recovery, the recovery is only done if the last recorded epoch is still the same as the global epoch. So, recoveries are safe. Here is a table that highlights general features in each counter type:",[123,124,125,146],"table",{},[126,127,128],"thead",{},[129,130,131,134,137,140,143],"tr",{},[132,133],"th",{},[132,135,136],{},"StrictCounter",[132,138,139],{},"LaxCounter",[132,141,142],{},"StrictInstanceAwareCounter",[132,144,145],{},"LaxInstanceAwareCounter",[147,148,149,168,188,205,222,237,263],"tbody",{},[129,150,151,158,161,164,166],{},[152,153,154],"td",{},[155,156,157],"strong",{},"Consistency",[152,159,160],{},"Immediate",[152,162,163],{},"Eventual",[152,165,160],{},[152,167,163],{},[129,169,170,178,181,184,186],{},[152,171,172],{},[155,173,174,177],{},[23,175,176],{},"inc"," latency",[152,179,180],{},"Redis round-trip",[152,182,183],{},"Sub-microsecond",[152,185,180],{},[152,187,183],{},[129,189,190,195,198,201,203],{},[152,191,192],{},[155,193,194],{},"Redis I/O",[152,196,197],{},"Every call",[152,199,200],{},"Batched",[152,202,197],{},[152,204,200],{},[129,206,207,212,215,217,220],{},[152,208,209],{},[155,210,211],{},"Per-instance tracking",[152,213,214],{},"No",[152,216,214],{},[152,218,219],{},"Yes",[152,221,219],{},[129,223,224,229,231,233,235],{},[152,225,226],{},[155,227,228],{},"Dead-instance cleanup",[152,230,214],{},[152,232,214],{},[152,234,219],{},[152,236,219],{},[129,238,239,244,250,254,259],{},[152,240,241],{},[155,242,243],{},"Feature flag",[152,245,246,249],{},[23,247,248],{},"counter"," (default)",[152,251,252,249],{},[23,253,248],{},[152,255,256],{},[23,257,258],{},"instance-aware-counter",[152,260,261],{},[23,262,258],{},[129,264,265,270,273,276,279],{},[152,266,267],{},[155,268,269],{},"Use case",[152,271,272],{},"Billing, inventory",[152,274,275],{},"High-throughput analytics",[152,277,278],{},"Live connection counts",[152,280,278],{},[28,282],{"className":283},[90],[285,286,136],"h3",{"id":287},"strictcounter",[28,289,292],{"className":290},[291],"mb-4",[19,293,294],{},"Every call is a single Redis round-trip executing an atomic Lua script. The stored value is always authoritative with no local state to go stale. Use this when accuracy is non-negotiable (billing, inventory, seat counts).",[296,297,302],"pre",{"className":298,"code":299,"language":300,"meta":301,"style":301},"language-toml shiki shiki-themes github-dark","[dependencies]\ndistkit = \"0.2\"\n","toml","",[23,303,304,320],{"__ignoreMap":301},[305,306,309,313,317],"span",{"class":307,"line":308},"line",1,[305,310,312],{"class":311},"s95oV","[",[305,314,316],{"class":315},"svObZ","dependencies",[305,318,319],{"class":311},"]\n",[305,321,323,326],{"class":307,"line":322},2,[305,324,325],{"class":311},"distkit = ",[305,327,329],{"class":328},"sU2Wk","\"0.2\"\n",[296,331,335],{"className":332,"code":333,"language":334,"meta":301,"style":301},"language-rust shiki shiki-themes github-dark","use distkit::{RedisKey, counter::{StrictCounter, CounterOptions, CounterTrait}};\n\nlet client = redis::Client::open(\"redis://127.0.0.1/\")?;\nlet conn = client.get_connection_manager().await?;\nlet prefix = RedisKey::try_from(\"my_app\".to_string())?;\nlet counter = StrictCounter::new(CounterOptions::new(prefix, conn));\n\nlet key = RedisKey::try_from(\"orders\".to_string())?;\ncounter.inc(&key, 1).await?;          // HINCRBY via Lua\ncounter.set(&key, 100).await?;        // HSET via Lua\nlet total = counter.get(&key).await?; // HGET\ncounter.del(&key).await?;             // HDEL, returns old value\ncounter.clear().await?;               // DEL on the hash\n","rust",[23,336,337,379,385,425,451,486,515,520,551,583,612,645,669],{"__ignoreMap":301},[305,338,339,343,346,349,352,355,358,360,362,364,366,368,371,373,376],{"class":307,"line":308},[305,340,342],{"class":341},"snl16","use",[305,344,345],{"class":315}," distkit",[305,347,348],{"class":341},"::",[305,350,351],{"class":311},"{",[305,353,354],{"class":315},"RedisKey",[305,356,357],{"class":311},", ",[305,359,248],{"class":315},[305,361,348],{"class":341},[305,363,351],{"class":311},[305,365,136],{"class":315},[305,367,357],{"class":311},[305,369,370],{"class":315},"CounterOptions",[305,372,357],{"class":311},[305,374,375],{"class":315},"CounterTrait",[305,377,378],{"class":311},"}};\n",[305,380,381],{"class":307,"line":322},[305,382,384],{"emptyLinePlaceholder":383},true,"\n",[305,386,388,391,394,397,400,402,405,407,410,413,416,419,422],{"class":307,"line":387},3,[305,389,390],{"class":341},"let",[305,392,393],{"class":311}," client ",[305,395,396],{"class":341},"=",[305,398,399],{"class":315}," redis",[305,401,348],{"class":341},[305,403,404],{"class":315},"Client",[305,406,348],{"class":341},[305,408,409],{"class":315},"open",[305,411,412],{"class":311},"(",[305,414,415],{"class":328},"\"redis://127.0.0.1/\"",[305,417,418],{"class":311},")",[305,420,421],{"class":341},"?",[305,423,424],{"class":311},";\n",[305,426,428,430,433,435,438,440,443,446,449],{"class":307,"line":427},4,[305,429,390],{"class":341},[305,431,432],{"class":311}," conn ",[305,434,396],{"class":341},[305,436,437],{"class":311}," client",[305,439,26],{"class":341},[305,441,442],{"class":315},"get_connection_manager",[305,444,445],{"class":311},"()",[305,447,448],{"class":341},".await?",[305,450,424],{"class":311},[305,452,454,456,459,461,464,466,469,471,474,476,479,482,484],{"class":307,"line":453},5,[305,455,390],{"class":341},[305,457,458],{"class":311}," prefix ",[305,460,396],{"class":341},[305,462,463],{"class":315}," RedisKey",[305,465,348],{"class":341},[305,467,468],{"class":315},"try_from",[305,470,412],{"class":311},[305,472,473],{"class":328},"\"my_app\"",[305,475,26],{"class":341},[305,477,478],{"class":315},"to_string",[305,480,481],{"class":311},"())",[305,483,421],{"class":341},[305,485,424],{"class":311},[305,487,489,491,494,496,499,501,504,506,508,510,512],{"class":307,"line":488},6,[305,490,390],{"class":341},[305,492,493],{"class":311}," counter ",[305,495,396],{"class":341},[305,497,498],{"class":315}," StrictCounter",[305,500,348],{"class":341},[305,502,503],{"class":315},"new",[305,505,412],{"class":311},[305,507,370],{"class":315},[305,509,348],{"class":341},[305,511,503],{"class":315},[305,513,514],{"class":311},"(prefix, conn));\n",[305,516,518],{"class":307,"line":517},7,[305,519,384],{"emptyLinePlaceholder":383},[305,521,523,525,528,530,532,534,536,538,541,543,545,547,549],{"class":307,"line":522},8,[305,524,390],{"class":341},[305,526,527],{"class":311}," key ",[305,529,396],{"class":341},[305,531,463],{"class":315},[305,533,348],{"class":341},[305,535,468],{"class":315},[305,537,412],{"class":311},[305,539,540],{"class":328},"\"orders\"",[305,542,26],{"class":341},[305,544,478],{"class":315},[305,546,481],{"class":311},[305,548,421],{"class":341},[305,550,424],{"class":311},[305,552,554,556,558,560,562,565,568,572,574,576,579],{"class":307,"line":553},9,[305,555,248],{"class":311},[305,557,26],{"class":341},[305,559,176],{"class":315},[305,561,412],{"class":311},[305,563,564],{"class":341},"&",[305,566,567],{"class":311},"key, ",[305,569,571],{"class":570},"sDLfK","1",[305,573,418],{"class":311},[305,575,448],{"class":341},[305,577,578],{"class":311},";          ",[305,580,582],{"class":581},"sAwPA","// HINCRBY via Lua\n",[305,584,586,588,590,593,595,597,599,602,604,606,609],{"class":307,"line":585},10,[305,587,248],{"class":311},[305,589,26],{"class":341},[305,591,592],{"class":315},"set",[305,594,412],{"class":311},[305,596,564],{"class":341},[305,598,567],{"class":311},[305,600,601],{"class":570},"100",[305,603,418],{"class":311},[305,605,448],{"class":341},[305,607,608],{"class":311},";        ",[305,610,611],{"class":581},"// HSET via Lua\n",[305,613,615,617,620,622,625,627,630,632,634,637,639,642],{"class":307,"line":614},11,[305,616,390],{"class":341},[305,618,619],{"class":311}," total ",[305,621,396],{"class":341},[305,623,624],{"class":311}," counter",[305,626,26],{"class":341},[305,628,629],{"class":315},"get",[305,631,412],{"class":311},[305,633,564],{"class":341},[305,635,636],{"class":311},"key)",[305,638,448],{"class":341},[305,640,641],{"class":311},"; ",[305,643,644],{"class":581},"// HGET\n",[305,646,648,650,652,655,657,659,661,663,666],{"class":307,"line":647},12,[305,649,248],{"class":311},[305,651,26],{"class":341},[305,653,654],{"class":315},"del",[305,656,412],{"class":311},[305,658,564],{"class":341},[305,660,636],{"class":311},[305,662,448],{"class":341},[305,664,665],{"class":311},";             ",[305,667,668],{"class":581},"// HDEL, returns old value\n",[305,670,672,674,676,679,681,683,686],{"class":307,"line":671},13,[305,673,248],{"class":311},[305,675,26],{"class":341},[305,677,678],{"class":315},"clear",[305,680,445],{"class":311},[305,682,448],{"class":341},[305,684,685],{"class":311},";               ",[305,687,688],{"class":581},"// DEL on the hash\n",[28,690],{"className":691},[90],[285,693,139],{"id":694},"laxcounter",[28,696,698],{"className":697},[291],[19,699,700,701,703,704,706],{},"When you can allow a small lag between writes and reads, use the ",[23,702,139],{},". It would be significantly faster. The main caveat is that we get an eventual consistency instead of immediate consistency the ",[23,705,136],{}," offers.",[296,708,710],{"className":332,"code":709,"language":334,"meta":301,"style":301},"use distkit::{RedisKey, counter::{LaxCounter, CounterOptions, CounterTrait}};\n\nlet counter = LaxCounter::new(CounterOptions::new(prefix, conn));\n\nlet key = RedisKey::try_from(\"impressions\".to_string())?;\ncounter.inc(&key, 1).await?;         // local atomic add, sub-microsecond\nlet val = counter.get(&key).await?;  // reads local state, no Redis hit\n",[23,711,712,744,748,773,777,806,832],{"__ignoreMap":301},[305,713,714,716,718,720,722,724,726,728,730,732,734,736,738,740,742],{"class":307,"line":308},[305,715,342],{"class":341},[305,717,345],{"class":315},[305,719,348],{"class":341},[305,721,351],{"class":311},[305,723,354],{"class":315},[305,725,357],{"class":311},[305,727,248],{"class":315},[305,729,348],{"class":341},[305,731,351],{"class":311},[305,733,139],{"class":315},[305,735,357],{"class":311},[305,737,370],{"class":315},[305,739,357],{"class":311},[305,741,375],{"class":315},[305,743,378],{"class":311},[305,745,746],{"class":307,"line":322},[305,747,384],{"emptyLinePlaceholder":383},[305,749,750,752,754,756,759,761,763,765,767,769,771],{"class":307,"line":387},[305,751,390],{"class":341},[305,753,493],{"class":311},[305,755,396],{"class":341},[305,757,758],{"class":315}," LaxCounter",[305,760,348],{"class":341},[305,762,503],{"class":315},[305,764,412],{"class":311},[305,766,370],{"class":315},[305,768,348],{"class":341},[305,770,503],{"class":315},[305,772,514],{"class":311},[305,774,775],{"class":307,"line":427},[305,776,384],{"emptyLinePlaceholder":383},[305,778,779,781,783,785,787,789,791,793,796,798,800,802,804],{"class":307,"line":453},[305,780,390],{"class":341},[305,782,527],{"class":311},[305,784,396],{"class":341},[305,786,463],{"class":315},[305,788,348],{"class":341},[305,790,468],{"class":315},[305,792,412],{"class":311},[305,794,795],{"class":328},"\"impressions\"",[305,797,26],{"class":341},[305,799,478],{"class":315},[305,801,481],{"class":311},[305,803,421],{"class":341},[305,805,424],{"class":311},[305,807,808,810,812,814,816,818,820,822,824,826,829],{"class":307,"line":488},[305,809,248],{"class":311},[305,811,26],{"class":341},[305,813,176],{"class":315},[305,815,412],{"class":311},[305,817,564],{"class":341},[305,819,567],{"class":311},[305,821,571],{"class":570},[305,823,418],{"class":311},[305,825,448],{"class":341},[305,827,828],{"class":311},";         ",[305,830,831],{"class":581},"// local atomic add, sub-microsecond\n",[305,833,834,836,839,841,843,845,847,849,851,853,855,858],{"class":307,"line":517},[305,835,390],{"class":341},[305,837,838],{"class":311}," val ",[305,840,396],{"class":341},[305,842,624],{"class":311},[305,844,26],{"class":341},[305,846,629],{"class":315},[305,848,412],{"class":311},[305,850,564],{"class":341},[305,852,636],{"class":311},[305,854,448],{"class":341},[305,856,857],{"class":311},";  ",[305,859,860],{"class":581},"// reads local state, no Redis hit\n",[28,862],{"className":863},[99],[92,865,867],{"id":866},"instance-aware-counters","Instance-aware counters",[19,869,870],{},"As mentioned above, the instance aware counters are a bit different from the normal counters. They track the counter on each instance and allow you to read the cumulative total. And they manage instance deaths and recovery.",[296,872,874],{"className":298,"code":873,"language":300,"meta":301,"style":301},"[dependencies]\ndistkit = { version = \"0.2\", features = [\"instance-aware-counter\"] }\n",[23,875,876,884],{"__ignoreMap":301},[305,877,878,880,882],{"class":307,"line":308},[305,879,312],{"class":311},[305,881,316],{"class":315},[305,883,319],{"class":311},[305,885,886,889,892,895,898],{"class":307,"line":322},[305,887,888],{"class":311},"distkit = { version = ",[305,890,891],{"class":328},"\"0.2\"",[305,893,894],{"class":311},", features = [",[305,896,897],{"class":328},"\"instance-aware-counter\"",[305,899,900],{"class":311},"] }\n",[28,902],{"className":903},[90],[285,905,142],{"id":906},"strictinstanceawarecounter",[28,908,910],{"className":909},[291],[19,911,912,913,915],{},"This is the ",[23,914,136],{}," with instance awareness. Every call is immediately consistent.",[296,917,919],{"className":332,"code":918,"language":334,"meta":301,"style":301},"use distkit::icounter::{\n    InstanceAwareCounterTrait,\n    StrictInstanceAwareCounter, StrictInstanceAwareCounterOptions,\n};\nuse distkit::RedisKey;\n\nlet counter = StrictInstanceAwareCounter::new(\n    StrictInstanceAwareCounterOptions::new(prefix, conn),\n);\n\nlet key = RedisKey::try_from(\"connections\".to_string())?;\n\n// All methods return (cumulative, this_instance_value).\nlet (total, mine) = counter.inc(&key, 5).await?;\nlet (total, mine) = counter.dec(&key, 2).await?;\nlet (total, mine) = counter.get(&key).await?;\n\n// Adjust only this instance's slice (no epoch bump).\nlet (total, mine) = counter.set_on_instance(&key, 10).await?;\n\n// Coordinate a global reset across all instances (bumps epoch).\nlet (total, mine) = counter.set(&key, 100).await?;\n\n// Remove only this instance's contribution.\nlet (total, removed) = counter.del_on_instance(&key).await?;\n\n// Delete the key globally and bump the epoch.\nlet (old_total, _) = counter.del(&key).await?;\n",[23,920,921,937,945,957,962,974,978,996,1008,1013,1017,1046,1050,1055,1086,1117,1142,1147,1153,1184,1189,1195,1224,1229,1235,1262,1267,1273],{"__ignoreMap":301},[305,922,923,925,927,929,932,934],{"class":307,"line":308},[305,924,342],{"class":341},[305,926,345],{"class":315},[305,928,348],{"class":341},[305,930,931],{"class":315},"icounter",[305,933,348],{"class":341},[305,935,936],{"class":311},"{\n",[305,938,939,942],{"class":307,"line":322},[305,940,941],{"class":315},"    InstanceAwareCounterTrait",[305,943,944],{"class":311},",\n",[305,946,947,950,952,955],{"class":307,"line":387},[305,948,949],{"class":315},"    StrictInstanceAwareCounter",[305,951,357],{"class":311},[305,953,954],{"class":315},"StrictInstanceAwareCounterOptions",[305,956,944],{"class":311},[305,958,959],{"class":307,"line":427},[305,960,961],{"class":311},"};\n",[305,963,964,966,968,970,972],{"class":307,"line":453},[305,965,342],{"class":341},[305,967,345],{"class":315},[305,969,348],{"class":341},[305,971,354],{"class":315},[305,973,424],{"class":311},[305,975,976],{"class":307,"line":488},[305,977,384],{"emptyLinePlaceholder":383},[305,979,980,982,984,986,989,991,993],{"class":307,"line":517},[305,981,390],{"class":341},[305,983,493],{"class":311},[305,985,396],{"class":341},[305,987,988],{"class":315}," StrictInstanceAwareCounter",[305,990,348],{"class":341},[305,992,503],{"class":315},[305,994,995],{"class":311},"(\n",[305,997,998,1001,1003,1005],{"class":307,"line":522},[305,999,1000],{"class":315},"    StrictInstanceAwareCounterOptions",[305,1002,348],{"class":341},[305,1004,503],{"class":315},[305,1006,1007],{"class":311},"(prefix, conn),\n",[305,1009,1010],{"class":307,"line":553},[305,1011,1012],{"class":311},");\n",[305,1014,1015],{"class":307,"line":585},[305,1016,384],{"emptyLinePlaceholder":383},[305,1018,1019,1021,1023,1025,1027,1029,1031,1033,1036,1038,1040,1042,1044],{"class":307,"line":614},[305,1020,390],{"class":341},[305,1022,527],{"class":311},[305,1024,396],{"class":341},[305,1026,463],{"class":315},[305,1028,348],{"class":341},[305,1030,468],{"class":315},[305,1032,412],{"class":311},[305,1034,1035],{"class":328},"\"connections\"",[305,1037,26],{"class":341},[305,1039,478],{"class":315},[305,1041,481],{"class":311},[305,1043,421],{"class":341},[305,1045,424],{"class":311},[305,1047,1048],{"class":307,"line":647},[305,1049,384],{"emptyLinePlaceholder":383},[305,1051,1052],{"class":307,"line":671},[305,1053,1054],{"class":581},"// All methods return (cumulative, this_instance_value).\n",[305,1056,1058,1060,1063,1065,1067,1069,1071,1073,1075,1077,1080,1082,1084],{"class":307,"line":1057},14,[305,1059,390],{"class":341},[305,1061,1062],{"class":311}," (total, mine) ",[305,1064,396],{"class":341},[305,1066,624],{"class":311},[305,1068,26],{"class":341},[305,1070,176],{"class":315},[305,1072,412],{"class":311},[305,1074,564],{"class":341},[305,1076,567],{"class":311},[305,1078,1079],{"class":570},"5",[305,1081,418],{"class":311},[305,1083,448],{"class":341},[305,1085,424],{"class":311},[305,1087,1089,1091,1093,1095,1097,1099,1102,1104,1106,1108,1111,1113,1115],{"class":307,"line":1088},15,[305,1090,390],{"class":341},[305,1092,1062],{"class":311},[305,1094,396],{"class":341},[305,1096,624],{"class":311},[305,1098,26],{"class":341},[305,1100,1101],{"class":315},"dec",[305,1103,412],{"class":311},[305,1105,564],{"class":341},[305,1107,567],{"class":311},[305,1109,1110],{"class":570},"2",[305,1112,418],{"class":311},[305,1114,448],{"class":341},[305,1116,424],{"class":311},[305,1118,1120,1122,1124,1126,1128,1130,1132,1134,1136,1138,1140],{"class":307,"line":1119},16,[305,1121,390],{"class":341},[305,1123,1062],{"class":311},[305,1125,396],{"class":341},[305,1127,624],{"class":311},[305,1129,26],{"class":341},[305,1131,629],{"class":315},[305,1133,412],{"class":311},[305,1135,564],{"class":341},[305,1137,636],{"class":311},[305,1139,448],{"class":341},[305,1141,424],{"class":311},[305,1143,1145],{"class":307,"line":1144},17,[305,1146,384],{"emptyLinePlaceholder":383},[305,1148,1150],{"class":307,"line":1149},18,[305,1151,1152],{"class":581},"// Adjust only this instance's slice (no epoch bump).\n",[305,1154,1156,1158,1160,1162,1164,1166,1169,1171,1173,1175,1178,1180,1182],{"class":307,"line":1155},19,[305,1157,390],{"class":341},[305,1159,1062],{"class":311},[305,1161,396],{"class":341},[305,1163,624],{"class":311},[305,1165,26],{"class":341},[305,1167,1168],{"class":315},"set_on_instance",[305,1170,412],{"class":311},[305,1172,564],{"class":341},[305,1174,567],{"class":311},[305,1176,1177],{"class":570},"10",[305,1179,418],{"class":311},[305,1181,448],{"class":341},[305,1183,424],{"class":311},[305,1185,1187],{"class":307,"line":1186},20,[305,1188,384],{"emptyLinePlaceholder":383},[305,1190,1192],{"class":307,"line":1191},21,[305,1193,1194],{"class":581},"// Coordinate a global reset across all instances (bumps epoch).\n",[305,1196,1198,1200,1202,1204,1206,1208,1210,1212,1214,1216,1218,1220,1222],{"class":307,"line":1197},22,[305,1199,390],{"class":341},[305,1201,1062],{"class":311},[305,1203,396],{"class":341},[305,1205,624],{"class":311},[305,1207,26],{"class":341},[305,1209,592],{"class":315},[305,1211,412],{"class":311},[305,1213,564],{"class":341},[305,1215,567],{"class":311},[305,1217,601],{"class":570},[305,1219,418],{"class":311},[305,1221,448],{"class":341},[305,1223,424],{"class":311},[305,1225,1227],{"class":307,"line":1226},23,[305,1228,384],{"emptyLinePlaceholder":383},[305,1230,1232],{"class":307,"line":1231},24,[305,1233,1234],{"class":581},"// Remove only this instance's contribution.\n",[305,1236,1238,1240,1243,1245,1247,1249,1252,1254,1256,1258,1260],{"class":307,"line":1237},25,[305,1239,390],{"class":341},[305,1241,1242],{"class":311}," (total, removed) ",[305,1244,396],{"class":341},[305,1246,624],{"class":311},[305,1248,26],{"class":341},[305,1250,1251],{"class":315},"del_on_instance",[305,1253,412],{"class":311},[305,1255,564],{"class":341},[305,1257,636],{"class":311},[305,1259,448],{"class":341},[305,1261,424],{"class":311},[305,1263,1265],{"class":307,"line":1264},26,[305,1266,384],{"emptyLinePlaceholder":383},[305,1268,1270],{"class":307,"line":1269},27,[305,1271,1272],{"class":581},"// Delete the key globally and bump the epoch.\n",[305,1274,1276,1278,1281,1283,1285,1287,1289,1291,1293,1295,1297],{"class":307,"line":1275},28,[305,1277,390],{"class":341},[305,1279,1280],{"class":311}," (old_total, _) ",[305,1282,396],{"class":341},[305,1284,624],{"class":311},[305,1286,26],{"class":341},[305,1288,654],{"class":315},[305,1290,412],{"class":311},[305,1292,564],{"class":341},[305,1294,636],{"class":311},[305,1296,448],{"class":341},[305,1298,424],{"class":311},[28,1300],{"className":1301},[90],[285,1303,145],{"id":1304},"laxinstanceawarecounter",[28,1306,1308],{"className":1307},[291],[19,1309,1310,1311,1313,1314,1316],{},"The ",[23,1312,145],{}," is a buffered wrapper around ",[23,1315,142],{},". It allowed for a small lag between writes and reads and batches the writes when necessary. This allowed for eventual consistency but at a high throughput since we are not paying the network price for each operation.",[296,1318,1320],{"className":332,"code":1319,"language":334,"meta":301,"style":301},"use distkit::icounter::{\n    InstanceAwareCounterTrait,\n    LaxInstanceAwareCounter, LaxInstanceAwareCounterOptions,\n};\nuse distkit::RedisKey;\nuse std::time::Duration;\n\nlet counter = LaxInstanceAwareCounter::new(LaxInstanceAwareCounterOptions {\n    prefix,\n    connection_manager: conn,\n    dead_instance_threshold_ms: 30_000,\n    flush_interval: Duration::from_millis(20),\n    allowed_lag:    Duration::from_millis(20),\n});\n\nlet key = RedisKey::try_from(\"connections\".to_string())?;\n\n// Warm path: returns local estimate with no Redis call.\nlet (local_total, mine) = counter.inc(&key, 1).await?;\nlet (local_total, mine) = counter.dec(&key, 1).await?;\nlet (total, mine)       = counter.get(&key).await?;\n",[23,1321,1322,1336,1342,1354,1358,1370,1389,1393,1415,1420,1431,1443,1466,1486,1491,1495,1523,1527,1532,1561,1589],{"__ignoreMap":301},[305,1323,1324,1326,1328,1330,1332,1334],{"class":307,"line":308},[305,1325,342],{"class":341},[305,1327,345],{"class":315},[305,1329,348],{"class":341},[305,1331,931],{"class":315},[305,1333,348],{"class":341},[305,1335,936],{"class":311},[305,1337,1338,1340],{"class":307,"line":322},[305,1339,941],{"class":315},[305,1341,944],{"class":311},[305,1343,1344,1347,1349,1352],{"class":307,"line":387},[305,1345,1346],{"class":315},"    LaxInstanceAwareCounter",[305,1348,357],{"class":311},[305,1350,1351],{"class":315},"LaxInstanceAwareCounterOptions",[305,1353,944],{"class":311},[305,1355,1356],{"class":307,"line":427},[305,1357,961],{"class":311},[305,1359,1360,1362,1364,1366,1368],{"class":307,"line":453},[305,1361,342],{"class":341},[305,1363,345],{"class":315},[305,1365,348],{"class":341},[305,1367,354],{"class":315},[305,1369,424],{"class":311},[305,1371,1372,1374,1377,1379,1382,1384,1387],{"class":307,"line":488},[305,1373,342],{"class":341},[305,1375,1376],{"class":315}," std",[305,1378,348],{"class":341},[305,1380,1381],{"class":315},"time",[305,1383,348],{"class":341},[305,1385,1386],{"class":315},"Duration",[305,1388,424],{"class":311},[305,1390,1391],{"class":307,"line":517},[305,1392,384],{"emptyLinePlaceholder":383},[305,1394,1395,1397,1399,1401,1404,1406,1408,1410,1412],{"class":307,"line":522},[305,1396,390],{"class":341},[305,1398,493],{"class":311},[305,1400,396],{"class":341},[305,1402,1403],{"class":315}," LaxInstanceAwareCounter",[305,1405,348],{"class":341},[305,1407,503],{"class":315},[305,1409,412],{"class":311},[305,1411,1351],{"class":315},[305,1413,1414],{"class":311}," {\n",[305,1416,1417],{"class":307,"line":553},[305,1418,1419],{"class":311},"    prefix,\n",[305,1421,1422,1425,1428],{"class":307,"line":585},[305,1423,1424],{"class":311},"    connection_manager",[305,1426,1427],{"class":341},":",[305,1429,1430],{"class":311}," conn,\n",[305,1432,1433,1436,1438,1441],{"class":307,"line":614},[305,1434,1435],{"class":311},"    dead_instance_threshold_ms",[305,1437,1427],{"class":341},[305,1439,1440],{"class":570}," 30_000",[305,1442,944],{"class":311},[305,1444,1445,1448,1450,1453,1455,1458,1460,1463],{"class":307,"line":647},[305,1446,1447],{"class":311},"    flush_interval",[305,1449,1427],{"class":341},[305,1451,1452],{"class":315}," Duration",[305,1454,348],{"class":341},[305,1456,1457],{"class":315},"from_millis",[305,1459,412],{"class":311},[305,1461,1462],{"class":570},"20",[305,1464,1465],{"class":311},"),\n",[305,1467,1468,1471,1473,1476,1478,1480,1482,1484],{"class":307,"line":671},[305,1469,1470],{"class":311},"    allowed_lag",[305,1472,1427],{"class":341},[305,1474,1475],{"class":315},"    Duration",[305,1477,348],{"class":341},[305,1479,1457],{"class":315},[305,1481,412],{"class":311},[305,1483,1462],{"class":570},[305,1485,1465],{"class":311},[305,1487,1488],{"class":307,"line":1057},[305,1489,1490],{"class":311},"});\n",[305,1492,1493],{"class":307,"line":1088},[305,1494,384],{"emptyLinePlaceholder":383},[305,1496,1497,1499,1501,1503,1505,1507,1509,1511,1513,1515,1517,1519,1521],{"class":307,"line":1119},[305,1498,390],{"class":341},[305,1500,527],{"class":311},[305,1502,396],{"class":341},[305,1504,463],{"class":315},[305,1506,348],{"class":341},[305,1508,468],{"class":315},[305,1510,412],{"class":311},[305,1512,1035],{"class":328},[305,1514,26],{"class":341},[305,1516,478],{"class":315},[305,1518,481],{"class":311},[305,1520,421],{"class":341},[305,1522,424],{"class":311},[305,1524,1525],{"class":307,"line":1144},[305,1526,384],{"emptyLinePlaceholder":383},[305,1528,1529],{"class":307,"line":1149},[305,1530,1531],{"class":581},"// Warm path: returns local estimate with no Redis call.\n",[305,1533,1534,1536,1539,1541,1543,1545,1547,1549,1551,1553,1555,1557,1559],{"class":307,"line":1155},[305,1535,390],{"class":341},[305,1537,1538],{"class":311}," (local_total, mine) ",[305,1540,396],{"class":341},[305,1542,624],{"class":311},[305,1544,26],{"class":341},[305,1546,176],{"class":315},[305,1548,412],{"class":311},[305,1550,564],{"class":341},[305,1552,567],{"class":311},[305,1554,571],{"class":570},[305,1556,418],{"class":311},[305,1558,448],{"class":341},[305,1560,424],{"class":311},[305,1562,1563,1565,1567,1569,1571,1573,1575,1577,1579,1581,1583,1585,1587],{"class":307,"line":1186},[305,1564,390],{"class":341},[305,1566,1538],{"class":311},[305,1568,396],{"class":341},[305,1570,624],{"class":311},[305,1572,26],{"class":341},[305,1574,1101],{"class":315},[305,1576,412],{"class":311},[305,1578,564],{"class":341},[305,1580,567],{"class":311},[305,1582,571],{"class":570},[305,1584,418],{"class":311},[305,1586,448],{"class":341},[305,1588,424],{"class":311},[305,1590,1591,1593,1596,1598,1600,1602,1604,1606,1608,1610,1612],{"class":307,"line":1191},[305,1592,390],{"class":341},[305,1594,1595],{"class":311}," (total, mine)       ",[305,1597,396],{"class":341},[305,1599,624],{"class":311},[305,1601,26],{"class":341},[305,1603,629],{"class":315},[305,1605,412],{"class":311},[305,1607,564],{"class":341},[305,1609,636],{"class":311},[305,1611,448],{"class":341},[305,1613,424],{"class":311},[28,1615],{"className":1616},[90],[285,1618,228],{"id":1619},"dead-instance-cleanup",[28,1621,1623],{"className":1622},[291],[19,1624,1625],{},"Each instance writes a liveness heartbeat to Redis on every flush. If a process silently dies, surviving instances detect the expired heartbeat and remove the orphaned contribution automatically the next time any of them touches the same key.",[296,1627,1629],{"className":332,"code":1628,"language":334,"meta":301,"style":301},"// Two separate processes / server instances:\nlet server_a = StrictInstanceAwareCounter::new(opts(conn1));\nlet server_b = StrictInstanceAwareCounter::new(opts(conn2));\n\nserver_a.inc(&key, 10).await?; // cumulative = 10\nserver_b.inc(&key,  5).await?; // cumulative = 15\n\n// server_a goes offline.\n// After dead_instance_threshold_ms (default 30 s), server_b's next call\n// removes server_a's contribution automatically.\nlet (total, _) = server_b.get(&key).await?; // total = 5 once cleaned up\n",[23,1630,1631,1636,1659,1681,1685,1711,1738,1742,1747,1752,1757],{"__ignoreMap":301},[305,1632,1633],{"class":307,"line":308},[305,1634,1635],{"class":581},"// Two separate processes / server instances:\n",[305,1637,1638,1640,1643,1645,1647,1649,1651,1653,1656],{"class":307,"line":322},[305,1639,390],{"class":341},[305,1641,1642],{"class":311}," server_a ",[305,1644,396],{"class":341},[305,1646,988],{"class":315},[305,1648,348],{"class":341},[305,1650,503],{"class":315},[305,1652,412],{"class":311},[305,1654,1655],{"class":315},"opts",[305,1657,1658],{"class":311},"(conn1));\n",[305,1660,1661,1663,1666,1668,1670,1672,1674,1676,1678],{"class":307,"line":387},[305,1662,390],{"class":341},[305,1664,1665],{"class":311}," server_b ",[305,1667,396],{"class":341},[305,1669,988],{"class":315},[305,1671,348],{"class":341},[305,1673,503],{"class":315},[305,1675,412],{"class":311},[305,1677,1655],{"class":315},[305,1679,1680],{"class":311},"(conn2));\n",[305,1682,1683],{"class":307,"line":427},[305,1684,384],{"emptyLinePlaceholder":383},[305,1686,1687,1690,1692,1694,1696,1698,1700,1702,1704,1706,1708],{"class":307,"line":453},[305,1688,1689],{"class":311},"server_a",[305,1691,26],{"class":341},[305,1693,176],{"class":315},[305,1695,412],{"class":311},[305,1697,564],{"class":341},[305,1699,567],{"class":311},[305,1701,1177],{"class":570},[305,1703,418],{"class":311},[305,1705,448],{"class":341},[305,1707,641],{"class":311},[305,1709,1710],{"class":581},"// cumulative = 10\n",[305,1712,1713,1716,1718,1720,1722,1724,1727,1729,1731,1733,1735],{"class":307,"line":488},[305,1714,1715],{"class":311},"server_b",[305,1717,26],{"class":341},[305,1719,176],{"class":315},[305,1721,412],{"class":311},[305,1723,564],{"class":341},[305,1725,1726],{"class":311},"key,  ",[305,1728,1079],{"class":570},[305,1730,418],{"class":311},[305,1732,448],{"class":341},[305,1734,641],{"class":311},[305,1736,1737],{"class":581},"// cumulative = 15\n",[305,1739,1740],{"class":307,"line":517},[305,1741,384],{"emptyLinePlaceholder":383},[305,1743,1744],{"class":307,"line":522},[305,1745,1746],{"class":581},"// server_a goes offline.\n",[305,1748,1749],{"class":307,"line":553},[305,1750,1751],{"class":581},"// After dead_instance_threshold_ms (default 30 s), server_b's next call\n",[305,1753,1754],{"class":307,"line":585},[305,1755,1756],{"class":581},"// removes server_a's contribution automatically.\n",[305,1758,1759,1761,1764,1766,1769,1771,1773,1775,1777,1779,1781,1783],{"class":307,"line":614},[305,1760,390],{"class":341},[305,1762,1763],{"class":311}," (total, _) ",[305,1765,396],{"class":341},[305,1767,1768],{"class":311}," server_b",[305,1770,26],{"class":341},[305,1772,629],{"class":315},[305,1774,412],{"class":311},[305,1776,564],{"class":341},[305,1778,636],{"class":311},[305,1780,448],{"class":341},[305,1782,641],{"class":311},[305,1784,1785],{"class":581},"// total = 5 once cleaned up\n",[28,1787],{"className":1788},[99],[92,1790,1792],{"id":1791},"rate-limiting","Rate limiting",[28,1794,1796],{"className":1795},[31],[19,1797,1798,1799,1801,1802,1808,1809,1427],{},"Enable the ",[23,1800,25],{}," feature to access sliding-window rate limiting. distkit re-exports the entire ",[45,1803,1806],{"href":1804,"rel":1805},"https://docs.rs/trypema",[49],[23,1807,25],{}," crate under ",[23,1810,1811],{},"distkit::trypema",[28,1813,1815],{"className":1814},[31],[19,1816,1817,1818,1823,1824,1829,1830],{},"To get more details on trypema, you can see the ",[45,1819,1822],{"href":1820,"rel":1821},"https://davidoyinbo.com/projects/trypema-rate-limiter",[49],"project overview",", the ",[45,1825,1828],{"href":1826,"rel":1827},"https://trypema.davidoyinbo.com/",[49],"api documentation"," or the ",[45,1831,1834],{"href":1832,"rel":1833},"https://docs.rs/trypema/",[49],"rust documentation",[296,1836,1838],{"className":298,"code":1837,"language":300,"meta":301,"style":301},"[dependencies]\ndistkit = { version = \"0.2\", features = [\"trypema\"] }\n",[23,1839,1840,1848],{"__ignoreMap":301},[305,1841,1842,1844,1846],{"class":307,"line":308},[305,1843,312],{"class":311},[305,1845,316],{"class":315},[305,1847,319],{"class":311},[305,1849,1850,1852,1854,1856,1859],{"class":307,"line":322},[305,1851,888],{"class":311},[305,1853,891],{"class":328},[305,1855,894],{"class":311},[305,1857,1858],{"class":328},"\"trypema\"",[305,1860,900],{"class":311},[28,1862,1864],{"className":1863},[291],[19,1865,1866,1867,1870,1871,1874,1875,1878,1879,1882,1883,1886],{},"Three providers are available: ",[155,1868,1869],{},"local"," (in-process, sub-microsecond), ",[155,1872,1873],{},"Redis"," (distributed, atomic Lua enforcement), and ",[155,1876,1877],{},"hybrid"," (local fast-path with periodic Redis sync). Two strategies are available: ",[155,1880,1881],{},"absolute"," (binary allow/reject sliding window) and ",[155,1884,1885],{},"suppressed"," (probabilistic degradation that ramps rejection probability near capacity).",[296,1888,1890],{"className":332,"code":1889,"language":334,"meta":301,"style":301},"use distkit::trypema::{RateLimit, RateLimitDecision, RateLimiter, RateLimiterOptions};\n\n// ... build rl: Arc\u003CRateLimiter> at startup ...\n\nlet rate = RateLimit::try_from(10.0)?; // 10 requests per second\n\nmatch rl.local().absolute().inc(\"user_123\", &rate, 1) {\n    RateLimitDecision::Allowed => { /* proceed */ }\n    RateLimitDecision::Rejected { retry_after_ms, .. } => {\n        eprintln!(\"Rate limited, retry in {retry_after_ms} ms\");\n    }\n    _ => {}\n}\n",[23,1891,1892,1926,1930,1935,1939,1969,1973,2014,2036,2059,2071,2076,2086],{"__ignoreMap":301},[305,1893,1894,1896,1898,1900,1902,1904,1906,1909,1911,1914,1916,1919,1921,1924],{"class":307,"line":308},[305,1895,342],{"class":341},[305,1897,345],{"class":315},[305,1899,348],{"class":341},[305,1901,25],{"class":315},[305,1903,348],{"class":341},[305,1905,351],{"class":311},[305,1907,1908],{"class":315},"RateLimit",[305,1910,357],{"class":311},[305,1912,1913],{"class":315},"RateLimitDecision",[305,1915,357],{"class":311},[305,1917,1918],{"class":315},"RateLimiter",[305,1920,357],{"class":311},[305,1922,1923],{"class":315},"RateLimiterOptions",[305,1925,961],{"class":311},[305,1927,1928],{"class":307,"line":322},[305,1929,384],{"emptyLinePlaceholder":383},[305,1931,1932],{"class":307,"line":387},[305,1933,1934],{"class":581},"// ... build rl: Arc\u003CRateLimiter> at startup ...\n",[305,1936,1937],{"class":307,"line":427},[305,1938,384],{"emptyLinePlaceholder":383},[305,1940,1941,1943,1946,1948,1951,1953,1955,1957,1960,1962,1964,1966],{"class":307,"line":453},[305,1942,390],{"class":341},[305,1944,1945],{"class":311}," rate ",[305,1947,396],{"class":341},[305,1949,1950],{"class":315}," RateLimit",[305,1952,348],{"class":341},[305,1954,468],{"class":315},[305,1956,412],{"class":311},[305,1958,1959],{"class":570},"10.0",[305,1961,418],{"class":311},[305,1963,421],{"class":341},[305,1965,641],{"class":311},[305,1967,1968],{"class":581},"// 10 requests per second\n",[305,1970,1971],{"class":307,"line":488},[305,1972,384],{"emptyLinePlaceholder":383},[305,1974,1975,1978,1981,1983,1985,1987,1989,1991,1993,1995,1997,1999,2002,2004,2006,2009,2011],{"class":307,"line":517},[305,1976,1977],{"class":341},"match",[305,1979,1980],{"class":311}," rl",[305,1982,26],{"class":341},[305,1984,1869],{"class":315},[305,1986,445],{"class":311},[305,1988,26],{"class":341},[305,1990,1881],{"class":315},[305,1992,445],{"class":311},[305,1994,26],{"class":341},[305,1996,176],{"class":315},[305,1998,412],{"class":311},[305,2000,2001],{"class":328},"\"user_123\"",[305,2003,357],{"class":311},[305,2005,564],{"class":341},[305,2007,2008],{"class":311},"rate, ",[305,2010,571],{"class":570},[305,2012,2013],{"class":311},") {\n",[305,2015,2016,2019,2021,2024,2027,2030,2033],{"class":307,"line":522},[305,2017,2018],{"class":315},"    RateLimitDecision",[305,2020,348],{"class":341},[305,2022,2023],{"class":315},"Allowed",[305,2025,2026],{"class":341}," =>",[305,2028,2029],{"class":311}," { ",[305,2031,2032],{"class":581},"/* proceed */",[305,2034,2035],{"class":311}," }\n",[305,2037,2038,2040,2042,2045,2048,2051,2054,2057],{"class":307,"line":553},[305,2039,2018],{"class":315},[305,2041,348],{"class":341},[305,2043,2044],{"class":315},"Rejected",[305,2046,2047],{"class":311}," { retry_after_ms, ",[305,2049,2050],{"class":341},"..",[305,2052,2053],{"class":311}," } ",[305,2055,2056],{"class":341},"=>",[305,2058,1414],{"class":311},[305,2060,2061,2064,2066,2069],{"class":307,"line":585},[305,2062,2063],{"class":315},"        eprintln!",[305,2065,412],{"class":311},[305,2067,2068],{"class":328},"\"Rate limited, retry in {retry_after_ms} ms\"",[305,2070,1012],{"class":311},[305,2072,2073],{"class":307,"line":614},[305,2074,2075],{"class":311},"    }\n",[305,2077,2078,2081,2083],{"class":307,"line":647},[305,2079,2080],{"class":311},"    _ ",[305,2082,2056],{"class":341},[305,2084,2085],{"class":311}," {}\n",[305,2087,2088],{"class":307,"line":671},[305,2089,2090],{"class":311},"}\n",[28,2092],{"className":2093},[99],[92,2095,2097],{"id":2096},"highlights","Highlights",[92,2099,2100],{"id":28},"::div",[92,2102,2104],{"id":2103},"class-grid-grid-cols-2-smgrid-cols-4-gap-4-mdgap-6","class: \"grid grid-cols-2 sm:grid-cols-4 gap-4 md:gap-6\"",[92,2106,2108],{"id":2107},"content-card",":::content-card",[92,2110,2112],{"id":2111},"class-col-span-2","class: \"col-span-2\"",[285,2114,2116],{"id":2115},"architecture","Architecture",[92,2118,2120],{"id":2119},"div-1","::::div",[92,2122,2124],{"id":2123},"class-text-xs-text-dune-600","class: \"text-xs text-dune-600\"",[2126,2127,2128,2132,2135,2142,2145],"ul",{},[2129,2130,2131],"li",{},"Four counter types across two feature flags",[2129,2133,2134],{},"Strict variants: atomic Lua scripts on every call",[2129,2136,2137,2138,2141],{},"Lax variants: in-memory ",[23,2139,2140],{},"DashMap"," buffer with background flush task",[2129,2143,2144],{},"Instance-aware variants: UUID-namespaced Redis hash fields",[2129,2146,2147,2148],{},"All backed by ",[23,2149,2150],{},"redis::aio::ConnectionManager",[19,2152,2153],{},"::::\n:::",[92,2155,2108],{"id":2156},"content-card-1",[92,2158,2112],{"id":2159},"class-col-span-2-1",[285,2161,2163],{"id":2162},"consistency-model","Consistency model",[92,2165,2120],{"id":2166},"div-2",[92,2168,2124],{"id":2169},"class-text-xs-text-dune-600-1",[2126,2171,2172,2175,2178,2186],{},[2129,2173,2174],{},"Strict counters: immediately consistent, no local state",[2129,2176,2177],{},"Lax counters: eventual consistency, configurable lag (default 20 ms)",[2129,2179,2180,2181,2183,2184],{},"Instance-aware: per-key epoch prevents double-counting after ",[23,2182,592],{},"/",[23,2185,654],{},[2129,2187,2188,2190],{},[23,2189,1168],{}," adjusts only this instance's slice without touching the epoch",[19,2192,2153],{},[92,2194,2108],{"id":2195},"content-card-2",[92,2197,2112],{"id":2198},"class-col-span-2-2",[285,2200,2202],{"id":2201},"warm-path-performance","Warm-path performance",[92,2204,2120],{"id":2205},"div-3",[92,2207,2124],{"id":2208},"class-text-xs-text-dune-600-2",[2126,2210,2211,2221,2229,2232],{},[2129,2212,2213,2216,2217,2220],{},[23,2214,2215],{},"LaxCounter::inc"," and ",[23,2218,2219],{},"LaxInstanceAwareCounter::inc",": sub-microsecond, pure in-memory atomics",[2129,2222,2223,2216,2226,2228],{},[23,2224,2225],{},"LaxInstanceAwareCounter::get",[23,2227,1168],{},": also in-memory, no Redis round-trip",[2129,2230,2231],{},"Flushes are batched into Redis pipelines on a configurable interval",[2129,2233,2234],{},"Background flush task drops automatically when the counter is dropped",[19,2236,2153],{},[92,2238,2108],{"id":2239},"content-card-3",[92,2241,2112],{"id":2242},"class-col-span-2-3",[285,2244,228],{"id":2245},"dead-instance-cleanup-1",[92,2247,2120],{"id":2248},"div-4",[92,2250,2124],{"id":2251},"class-text-xs-text-dune-600-3",[2126,2253,2254,2261,2264,2271],{},[2129,2255,2256,2257,2260],{},"Each instance writes a heartbeat to ",[23,2258,2259],{},"\u003Cprefix>:liveness:\u003Cuuid>"," on every flush",[2129,2262,2263],{},"Surviving instances scan liveness keys and remove expired contributions automatically",[2129,2265,2266,2267,2270],{},"Configurable threshold via ",[23,2268,2269],{},"dead_instance_threshold_ms"," (default 30 s)",[2129,2272,2273],{},"No manual cleanup needed; works correctly after crashes and ungraceful shutdowns",[19,2275,2153],{},[92,2277,2108],{"id":2278},"content-card-4",[92,2280,2112],{"id":2281},"class-col-span-2-4",[285,2283,2285],{"id":2284},"safety-correctness","Safety & correctness",[92,2287,2120],{"id":2288},"div-5",[92,2290,2124],{"id":2291},"class-text-xs-text-dune-600-4",[2126,2293,2294,2300,2306,2309],{},[2129,2295,2296,2299],{},[23,2297,2298],{},"#![forbid(unsafe_code)]",": zero unsafe blocks in the library",[2129,2301,2302,2303],{},"No panics in library code; all fallible paths return ",[23,2304,2305],{},"DistkitError",[2129,2307,2308],{},"57 runnable doc-test examples, each isolated to a unique Redis key prefix",[2129,2310,2311],{},"Criterion benchmarks for all four counter types across all operations",[19,2313,2153],{},[92,2315,2108],{"id":2316},"content-card-5",[92,2318,2112],{"id":2319},"class-col-span-2-5",[285,2321,2323],{"id":2322},"feature-flags","Feature flags",[92,2325,2120],{"id":2326},"div-6",[92,2328,2124],{"id":2329},"class-text-xs-text-dune-600-5",[2126,2331,2332,2341,2350,2355],{},[2129,2333,2334,2336,2337,2216,2339],{},[23,2335,248],{}," (default): ",[23,2338,136],{},[23,2340,139],{},[2129,2342,2343,2345,2346,2216,2348],{},[23,2344,258],{},": ",[23,2347,142],{},[23,2349,145],{},[2129,2351,2352,2354],{},[23,2353,25],{},": sliding-window rate limiting via the trypema crate",[2129,2356,2357],{},"All flags are independent and composable",[19,2359,2153],{},[19,2361,348],{},[28,2363,2366,2374,2382,2390],{"className":2364},[39,40,41,42,2365],"mt-8",[45,2367,2370,2372],{"href":47,"rel":2368,"className":2369,"target":56},[49],[51,52,53,54,55],[19,2371,59],{},[61,2373],{"icon":63},[45,2375,2378,2380],{"href":66,"rel":2376,"className":2377,"target":56},[49],[51,52,53,54,69,70,71],[19,2379,74],{},[61,2381],{"icon":63},[45,2383,2386,2388],{"href":79,"rel":2384,"className":2385,"target":56},[49],[51,52,53,54,69,70,71],[19,2387,84],{},[61,2389],{"icon":63},[45,2391,2394],{"href":2392,"className":2393},"/projects",[51,52,53,54,69,70,71],[19,2395,2396],{},"Back to Projects",[2398,2399,2400],"style",{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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 pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":301,"searchDepth":322,"depth":322,"links":2402},[2403,2404,2408,2413,2414,2415,2416,2417,2418,2421,2422,2423,2424,2427,2428,2429,2430,2433,2434,2435,2436,2439,2440,2441,2442,2445,2446,2447,2448,2451,2452],{"id":94,"depth":322,"text":95},{"id":108,"depth":322,"text":109,"children":2405},[2406,2407],{"id":287,"depth":387,"text":136},{"id":694,"depth":387,"text":139},{"id":866,"depth":322,"text":867,"children":2409},[2410,2411,2412],{"id":906,"depth":387,"text":142},{"id":1304,"depth":387,"text":145},{"id":1619,"depth":387,"text":228},{"id":1791,"depth":322,"text":1792},{"id":2096,"depth":322,"text":2097},{"id":28,"depth":322,"text":2100},{"id":2103,"depth":322,"text":2104},{"id":2107,"depth":322,"text":2108},{"id":2111,"depth":322,"text":2112,"children":2419},[2420],{"id":2115,"depth":387,"text":2116},{"id":2119,"depth":322,"text":2120},{"id":2123,"depth":322,"text":2124},{"id":2156,"depth":322,"text":2108},{"id":2159,"depth":322,"text":2112,"children":2425},[2426],{"id":2162,"depth":387,"text":2163},{"id":2166,"depth":322,"text":2120},{"id":2169,"depth":322,"text":2124},{"id":2195,"depth":322,"text":2108},{"id":2198,"depth":322,"text":2112,"children":2431},[2432],{"id":2201,"depth":387,"text":2202},{"id":2205,"depth":322,"text":2120},{"id":2208,"depth":322,"text":2124},{"id":2239,"depth":322,"text":2108},{"id":2242,"depth":322,"text":2112,"children":2437},[2438],{"id":2245,"depth":387,"text":228},{"id":2248,"depth":322,"text":2120},{"id":2251,"depth":322,"text":2124},{"id":2278,"depth":322,"text":2108},{"id":2281,"depth":322,"text":2112,"children":2443},[2444],{"id":2284,"depth":387,"text":2285},{"id":2288,"depth":322,"text":2120},{"id":2291,"depth":322,"text":2124},{"id":2316,"depth":322,"text":2108},{"id":2319,"depth":322,"text":2112,"children":2449},[2450],{"id":2322,"depth":387,"text":2323},{"id":2326,"depth":322,"text":2120},{"id":2329,"depth":322,"text":2124},"2026-04-03T00:00:00.000Z","Distributed systems primitives for Rust. Strict and lax counters, instance-aware counters with automatic dead-instance cleanup, and sliding-window rate limiting, all backed by Redis.","md","/projects/distkit/main.png",{"license":2458},"MIT","/projects/distkit",{"title":5,"description":2454},"projects/distkit",[2463,1873,2464,2465,2466],"Rust","Distributed Systems","Library","Async","nmmSFDe377EAEV3w3FzPf8iWMDTDkHSC2Kee1EwHF1Q",[2469,3018,4579],{"id":2470,"title":2471,"body":2472,"date":3004,"description":3005,"extension":2455,"image_url":2532,"link":2497,"meta":3006,"navigation":383,"path":3008,"seo":3009,"stem":3010,"tags":3011,"__hash__":3017},"projects/projects/payaza-web-sdk.md","Payaza Web SDK",{"type":7,"value":2473,"toc":2996},[2474,2478,2481,2484,2488,2491,2525,2533,2535,2542,2544,2645,2648,2652,2656,2950,2953,2957,2977,2993],[10,2475],{"className":2476,"date":2477},[13],"2021-10-01",[16,2479,2471],{"id":2480},"payaza-web-sdk",[19,2482,2483],{},"Note: This was built as part of my work at Payaza Africa. I focused on the SDK design, integration ergonomics, and developer documentation—not the entire Payaza product.",[28,2485],{"className":2486},[2487],"mb-3",[33,2489],{":tags":2490},"[\"SDK\", \"JavaScript\", \"Payments\", \"Nuxt.js\", \"Web\"]",[28,2492,2494,2505,2515],{"className":2493},[39,40,41,42,43,31],[45,2495,2500,2503],{"className":2496,"href":2497,"rel":2498,"target":2499},[51,52,53,54,55],"https://www.npmjs.com/package/@payaza/web-sdk",[49],"_blank",[19,2501,2502],{},"NPM Package",[61,2504],{"icon":63},[45,2506,2510,2513],{"className":2507,"href":2508,"rel":2509,"target":2499},[51,52,53,54,69,70,71],"https://payaza.africa/",[49],[19,2511,2512],{},"Visit Payaza",[61,2514],{"icon":63},[45,2516,2520,2523],{"className":2517,"href":2518,"rel":2519,"target":2499},[51,52,53,54,69,70,71],"https://payaza.africa/checkout/",[49],[19,2521,2522],{},"Try Checkout",[61,2524],{"icon":63},[2526,2527],"content-main-image",{"alt":2528,"className":2529,"image":2532},"payaza web sdk",[2530,2531],"my-16","h-[30vh]","/projects/payaza-web-sdk/main.png",[92,2534,95],{"id":94},[2536,2537,2539],"content-overview",{"className":2538},[99],[19,2540,2541],{},"The Payaza Web SDK provides a straightforward way to embed Payaza checkout into web apps, handling configuration, initialization, and event hooks with a clean API. Built during my tenure at Payaza to improve developer onboarding and accelerate integrations.\nMy contributions included building the embeddable Vue checkout application loaded by the SDK, implementing a Node.js reverse proxy that adds real‑time communication via Socket.IO to the backend, and designing/publishing the @payaza/web-sdk NPM package.",[92,2543,2097],{"id":2096},[28,2545,2552,2579,2603,2624],{"className":2546},[2547,2548,2549,2550,2551],"grid","grid-cols-2","sm:grid-cols-4","gap-4","md:gap-6",[2107,2553,2556,2560],{"className":2554},[2555],"col-span-2",[285,2557,2559],{"id":2558},"sdk-features","SDK Features",[28,2561,2565],{"className":2562},[2563,2564],"text-xs","text-dune-600",[2126,2566,2567,2570,2573,2576],{},[2129,2568,2569],{},"Lightweight, simple initialization",[2129,2571,2572],{},"Configurable callbacks and events",[2129,2574,2575],{},"Seamless checkout invocation",[2129,2577,2578],{},"Clear error states and typed payloads",[2107,2580,2582,2586],{"className":2581},[2555],[285,2583,2585],{"id":2584},"developer-experience","Developer Experience",[28,2587,2589],{"className":2588},[2563,2564],[2126,2590,2591,2594,2597,2600],{},[2129,2592,2593],{},"Usage guides and examples",[2129,2595,2596],{},"Minimal setup steps",[2129,2598,2599],{},"Works with modern frameworks (Nuxt/Vue, React)",[2129,2601,2602],{},"Focus on integration ergonomics",[2107,2604,2606,2610],{"className":2605},[2555],[285,2607,2609],{"id":2608},"my-contribution","My Contribution",[28,2611,2613],{"className":2612},[2563,2564],[2126,2614,2615,2618,2621],{},[2129,2616,2617],{},"Built the embeddable checkout application (Vue) loaded by the SDK package",[2129,2619,2620],{},"Implemented a Node.js reverse proxy to add real‑time communication via Socket.IO",[2129,2622,2623],{},"Designed and published the @payaza/web-sdk NPM package",[2107,2625,2627,2631],{"className":2626},[2555],[285,2628,2630],{"id":2629},"outcome","Outcome",[28,2632,2634],{"className":2633},[2563,2564],[2126,2635,2636,2639,2642],{},[2129,2637,2638],{},"Faster onboarding for partners",[2129,2640,2641],{},"Reduced integration friction",[2129,2643,2644],{},"Consistent checkout experiences across apps",[28,2646],{"className":2647},[99],[92,2649,2651],{"id":2650},"code-snippets","Code Snippets",[285,2653,2655],{"id":2654},"initialize-and-invoke-checkout","Initialize and Invoke Checkout",[296,2657,2662],{"className":2658,"code":2659,"filename":2660,"language":2661,"meta":301,"style":301},"language-html shiki shiki-themes github-dark","\u003Cscript src=\"https://cdn.payaza.africa/sdk/web.min.js\">\u003C/script>\n\u003Cscript>\n  document.addEventListener('DOMContentLoaded', function () {\n    const payaza = PayazaWebSDK.init({\n      publicKey: 'pk_test_XXXXXXXXXXXXXXXX',\n      customer: { email: 'user@example.com' },\n      amount: 5000, // in NGN kobo\n      currency: 'NGN',\n      onSuccess: (ref) => console.log('Success:', ref),\n      onClose: () => console.log('Closed'),\n      onError: (err) => console.error('Error:', err),\n    });\n\n    document.getElementById('pay-button').addEventListener('click', () => {\n      payaza.checkout();\n    });\n  });\n\u003C/script>\n\u003Cbutton id=\"pay-button\">Pay with Payaza\u003C/button>\n","index.html","html",[23,2663,2664,2689,2697,2718,2738,2748,2759,2772,2782,2813,2834,2861,2866,2870,2900,2911,2915,2920,2929],{"__ignoreMap":301},[305,2665,2666,2669,2673,2676,2678,2681,2684,2686],{"class":307,"line":308},[305,2667,2668],{"class":311},"\u003C",[305,2670,2672],{"class":2671},"s4JwU","script",[305,2674,2675],{"class":315}," src",[305,2677,396],{"class":311},[305,2679,2680],{"class":328},"\"https://cdn.payaza.africa/sdk/web.min.js\"",[305,2682,2683],{"class":311},">\u003C/",[305,2685,2672],{"class":2671},[305,2687,2688],{"class":311},">\n",[305,2690,2691,2693,2695],{"class":307,"line":322},[305,2692,2668],{"class":311},[305,2694,2672],{"class":2671},[305,2696,2688],{"class":311},[305,2698,2699,2702,2705,2707,2710,2712,2715],{"class":307,"line":387},[305,2700,2701],{"class":311},"  document.",[305,2703,2704],{"class":315},"addEventListener",[305,2706,412],{"class":311},[305,2708,2709],{"class":328},"'DOMContentLoaded'",[305,2711,357],{"class":311},[305,2713,2714],{"class":341},"function",[305,2716,2717],{"class":311}," () {\n",[305,2719,2720,2723,2726,2729,2732,2735],{"class":307,"line":427},[305,2721,2722],{"class":341},"    const",[305,2724,2725],{"class":570}," payaza",[305,2727,2728],{"class":341}," =",[305,2730,2731],{"class":311}," PayazaWebSDK.",[305,2733,2734],{"class":315},"init",[305,2736,2737],{"class":311},"({\n",[305,2739,2740,2743,2746],{"class":307,"line":453},[305,2741,2742],{"class":311},"      publicKey: ",[305,2744,2745],{"class":328},"'pk_test_XXXXXXXXXXXXXXXX'",[305,2747,944],{"class":311},[305,2749,2750,2753,2756],{"class":307,"line":488},[305,2751,2752],{"class":311},"      customer: { email: ",[305,2754,2755],{"class":328},"'user@example.com'",[305,2757,2758],{"class":311}," },\n",[305,2760,2761,2764,2767,2769],{"class":307,"line":517},[305,2762,2763],{"class":311},"      amount: ",[305,2765,2766],{"class":570},"5000",[305,2768,357],{"class":311},[305,2770,2771],{"class":581},"// in NGN kobo\n",[305,2773,2774,2777,2780],{"class":307,"line":522},[305,2775,2776],{"class":311},"      currency: ",[305,2778,2779],{"class":328},"'NGN'",[305,2781,944],{"class":311},[305,2783,2784,2787,2790,2794,2797,2799,2802,2805,2807,2810],{"class":307,"line":553},[305,2785,2786],{"class":315},"      onSuccess",[305,2788,2789],{"class":311},": (",[305,2791,2793],{"class":2792},"s9osk","ref",[305,2795,2796],{"class":311},") ",[305,2798,2056],{"class":341},[305,2800,2801],{"class":311}," console.",[305,2803,2804],{"class":315},"log",[305,2806,412],{"class":311},[305,2808,2809],{"class":328},"'Success:'",[305,2811,2812],{"class":311},", ref),\n",[305,2814,2815,2818,2821,2823,2825,2827,2829,2832],{"class":307,"line":585},[305,2816,2817],{"class":315},"      onClose",[305,2819,2820],{"class":311},": () ",[305,2822,2056],{"class":341},[305,2824,2801],{"class":311},[305,2826,2804],{"class":315},[305,2828,412],{"class":311},[305,2830,2831],{"class":328},"'Closed'",[305,2833,1465],{"class":311},[305,2835,2836,2839,2841,2844,2846,2848,2850,2853,2855,2858],{"class":307,"line":614},[305,2837,2838],{"class":315},"      onError",[305,2840,2789],{"class":311},[305,2842,2843],{"class":2792},"err",[305,2845,2796],{"class":311},[305,2847,2056],{"class":341},[305,2849,2801],{"class":311},[305,2851,2852],{"class":315},"error",[305,2854,412],{"class":311},[305,2856,2857],{"class":328},"'Error:'",[305,2859,2860],{"class":311},", err),\n",[305,2862,2863],{"class":307,"line":647},[305,2864,2865],{"class":311},"    });\n",[305,2867,2868],{"class":307,"line":671},[305,2869,384],{"emptyLinePlaceholder":383},[305,2871,2872,2875,2878,2880,2883,2886,2888,2890,2893,2896,2898],{"class":307,"line":1057},[305,2873,2874],{"class":311},"    document.",[305,2876,2877],{"class":315},"getElementById",[305,2879,412],{"class":311},[305,2881,2882],{"class":328},"'pay-button'",[305,2884,2885],{"class":311},").",[305,2887,2704],{"class":315},[305,2889,412],{"class":311},[305,2891,2892],{"class":328},"'click'",[305,2894,2895],{"class":311},", () ",[305,2897,2056],{"class":341},[305,2899,1414],{"class":311},[305,2901,2902,2905,2908],{"class":307,"line":1088},[305,2903,2904],{"class":311},"      payaza.",[305,2906,2907],{"class":315},"checkout",[305,2909,2910],{"class":311},"();\n",[305,2912,2913],{"class":307,"line":1119},[305,2914,2865],{"class":311},[305,2916,2917],{"class":307,"line":1144},[305,2918,2919],{"class":311},"  });\n",[305,2921,2922,2925,2927],{"class":307,"line":1149},[305,2923,2924],{"class":311},"\u003C/",[305,2926,2672],{"class":2671},[305,2928,2688],{"class":311},[305,2930,2931,2933,2935,2938,2940,2943,2946,2948],{"class":307,"line":1155},[305,2932,2668],{"class":311},[305,2934,51],{"class":2671},[305,2936,2937],{"class":315}," id",[305,2939,396],{"class":311},[305,2941,2942],{"class":328},"\"pay-button\"",[305,2944,2945],{"class":311},">Pay with Payaza\u003C/",[305,2947,51],{"class":2671},[305,2949,2688],{"class":311},[28,2951],{"className":2952},[99],[92,2954,2956],{"id":2955},"gallery","Gallery",[28,2958,2963,2969,2973],{"className":2959},[2547,2960,2961,2550,2551,2962],"grid-cols-1","xs:grid-cols-[repeat(auto-fill,minmax(280px,1fr))]","mb-8",[2964,2965],"content-image",{"className":2966,"image":2968},[2967],"h-[220px]","/projects/payaza-web-sdk/1.png",[2964,2970],{"className":2971,"image":2972},[2967],"/projects/payaza-web-sdk/2.png",[2964,2974],{"className":2975,"image":2976},[2967],"/projects/payaza-web-sdk/3.png",[28,2978,2980,2988],{"className":2979},[39,40,41,42],[45,2981,2984,2986],{"className":2982,"href":2497,"rel":2983,"target":2499},[51,52,53,54,55],[49],[19,2985,2502],{},[61,2987],{"icon":63},[45,2989,2991],{"className":2990,"href":2392},[51,52,53,54,69,70,71],[19,2992,2396],{},[2398,2994,2995],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}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);}",{"title":301,"searchDepth":322,"depth":322,"links":2997},[2998,2999,3000,3003],{"id":94,"depth":322,"text":95},{"id":2096,"depth":322,"text":2097},{"id":2650,"depth":322,"text":2651,"children":3001},[3002],{"id":2654,"depth":387,"text":2655},{"id":2955,"depth":322,"text":2956},"2021-10-01T00:00:00.000Z","A JavaScript Web SDK that simplifies integrating Payaza checkout on web applications. Built as part of my role at Payaza Africa.",{"license":3007},null,"/projects/payaza-web-sdk",{"title":2471,"description":3005},"projects/payaza-web-sdk",[3012,3013,3014,3015,3016],"SDK","JavaScript","Payments","Nuxt.js","Web","ZkzkMQ4iP0O_2gg9fqF5BP-yVh2gOR6pcvUnqk2Gces",{"id":3019,"title":3020,"body":3021,"date":4564,"description":4565,"extension":2455,"image_url":4566,"link":3047,"meta":4567,"navigation":383,"path":4568,"seo":4569,"stem":4570,"tags":4571,"__hash__":4578},"projects/projects/actix-web-starter-template.md","Actix Web Starter Template",{"type":7,"value":3022,"toc":4552},[3023,3027,3030,3033,3036,3039,3042,3065,3069,3071,3112,3114,3307,3310,3314,3364,3367,3369,3373,3709,3712,3716,3840,3843,3847,4075,4078,4082,4485,4488,4492,4540,4543,4549],[10,3024],{"className":3025,"date":3026},[13],"2025-06-27",[16,3028,3020],{"id":3029},"actix-web-starter-template",[19,3031,3032],{},"Production-ready Actix Web API template with RBAC auth, PostgreSQL via SeaORM, Kafka-powered email, and a full Docker dev setup.",[28,3034],{"className":3035},[2487],[33,3037],{":tags":3038},"[\"Rust\", \"Actix Web\", \"SeaORM\", \"PostgreSQL\", \"Kafka\", \"SMTP\", \"RBAC\"]",[28,3040],{"className":3041},[2962],[28,3043,3045,3055],{"className":3044},[39,40,41,42,31],[45,3046,3050,3053],{"href":3047,"rel":3048,"className":3049,"target":56},"https://github.com/dev-davexoyinbo/actix-web-starter-template",[49],[51,52,53,54,55],[19,3051,3052],{},"View on GitHub",[61,3054],{"icon":63},[45,3056,3060,3063],{"href":3057,"rel":3058,"className":3059,"target":56},"https://documenter.getpostman.com/view/34761218/2sB2jAa815",[49],[51,52,53,54,69,70,71],[19,3061,3062],{},"Postman Docs",[61,3064],{"icon":63},[28,3066],{"className":3067},[3068],"mb-16",[92,3070,95],{"id":94},[2536,3072,3074,3077,3080,3086],{"className":3073},[99],[19,3075,3076],{},"A robust template for building secure, scalable REST APIs in Rust using Actix Web.\nIncludes token-based auth with roles/permissions, email verification via OTP, background jobs, and observability.\nDocker Compose spins up PostgreSQL, Kafka, and Kafka UI for a smooth local dev flow.",[28,3078],{"className":3079},[291],[19,3081,3082,3083],{},"For detailed documentation, visit the GitHub repository: ",[45,3084,3047],{"href":3047,"rel":3085},[49],[3087,3088,3089],"template",{"v-slot:right":301},[28,3090,3092],{"className":3091},[2564,2563],[2126,3093,3094,3097,3100,3103,3106,3109],{},[2129,3095,3096],{},"Token auth (non‑JWT), Argon2 hashes, email OTP verification",[2129,3098,3099],{},"RBAC: roles, permissions, and route-level middleware",[2129,3101,3102],{},"PostgreSQL + SeaORM entities, migrations, and seeders",[2129,3104,3105],{},"Kafka + SMTP for async email delivery (Askama templates)",[2129,3107,3108],{},"Tracing logs and request correlation IDs",[2129,3110,3111],{},"Makefile-driven DX; .env-based configuration",[92,3113,2097],{"id":2096},[28,3115,3117,3141,3165,3189,3213,3237,3261,3285],{"className":3116},[2547,2548,2549,2550,2551],[2107,3118,3120,3124],{"className":3119},[2555],[285,3121,3123],{"id":3122},"authentication-access-control","Authentication & Access Control",[28,3125,3127],{"className":3126},[2563,2564],[2126,3128,3129,3132,3135,3138],{},[2129,3130,3131],{},"Token auth (non-JWT)",[2129,3133,3134],{},"RBAC roles and permissions",[2129,3136,3137],{},"Email verification (OTP)",[2129,3139,3140],{},"Route middleware for roles/permissions/conditions",[2107,3142,3144,3148],{"className":3143},[2555],[285,3145,3147],{"id":3146},"database","Database",[28,3149,3151],{"className":3150},[2563,2564],[2126,3152,3153,3156,3159,3162],{},[2129,3154,3155],{},"PostgreSQL + SeaORM",[2129,3157,3158],{},"Entities with relations",[2129,3160,3161],{},"Migrations + seeders",[2129,3163,3164],{},"Separate test DB config",[2107,3166,3168,3172],{"className":3167},[2555],[285,3169,3171],{"id":3170},"api-middleware","API & Middleware",[28,3173,3175],{"className":3174},[2563,2564],[2126,3176,3177,3180,3183,3186],{},[2129,3178,3179],{},"RESTful endpoints with DTO validation",[2129,3181,3182],{},"Pagination and structured errors",[2129,3184,3185],{},"Rate limiting and CORS",[2129,3187,3188],{},"Health/readiness endpoints",[2107,3190,3192,3196],{"className":3191},[2555],[285,3193,3195],{"id":3194},"messaging-email","Messaging & Email",[28,3197,3199],{"className":3198},[2563,2564],[2126,3200,3201,3204,3207,3210],{},[2129,3202,3203],{},"Kafka producers/consumers",[2129,3205,3206],{},"Async email via SMTP",[2129,3208,3209],{},"Askama HTML templates",[2129,3211,3212],{},"Topic-based prioritization",[2107,3214,3216,3220],{"className":3215},[2555],[285,3217,3219],{"id":3218},"observability","Observability",[28,3221,3223],{"className":3222},[2563,2564],[2126,3224,3225,3228,3231,3234],{},[2129,3226,3227],{},"tracing + tracing-actix-web",[2129,3229,3230],{},"Correlation IDs",[2129,3232,3233],{},"Debuggable error types",[2129,3235,3236],{},"Performance-ready hooks",[2107,3238,3240,3244],{"className":3239},[2555],[285,3241,3243],{"id":3242},"security","Security",[28,3245,3247],{"className":3246},[2563,2564],[2126,3248,3249,3252,3255,3258],{},[2129,3250,3251],{},"Argon2 password hashing",[2129,3253,3254],{},"Request size limits",[2129,3256,3257],{},"CORS and rate limiting",[2129,3259,3260],{},"Secret-driven config",[2107,3262,3264,3268],{"className":3263},[2555],[285,3265,3267],{"id":3266},"background-jobs","Background Jobs",[28,3269,3271],{"className":3270},[2563,2564],[2126,3272,3273,3276,3279,3282],{},[2129,3274,3275],{},"actix-jobs scheduler",[2129,3277,3278],{},"Token cleanup job",[2129,3280,3281],{},"Extensible job registry",[2129,3283,3284],{},"Cron-style schedules",[2107,3286,3288,3290],{"className":3287},[2555],[285,3289,2585],{"id":2584},[28,3291,3293],{"className":3292},[2563,2564],[2126,3294,3295,3298,3301,3304],{},[2129,3296,3297],{},"Docker Compose (PostgreSQL, Kafka, Kafka UI)",[2129,3299,3300],{},"Makefile (watch, test, migrate)",[2129,3302,3303],{},"bacon for live reload",[2129,3305,3306],{},".env configuration",[28,3308],{"className":3309},[99],[92,3311,3313],{"id":3312},"quick-start","Quick Start",[2107,3315,3316,3358],{},[28,3317,3319],{"className":3318},[2563,2564],[2126,3320,3321,3324,3335,3341,3347],{},[2129,3322,3323],{},"Rust 1.86+, Docker, Docker Compose",[2129,3325,3326,3327,3330,3331,3334],{},"Copy ",[23,3328,3329],{},".env.example"," to ",[23,3332,3333],{},".env"," and set values",[2129,3336,3337,3338],{},"Start services: ",[23,3339,3340],{},"make up-dev",[2129,3342,3343,3344],{},"Run migrations (with seeding): ",[23,3345,3346],{},"make migrate",[2129,3348,3349,3350,3353,3354],{},"Start app: ",[23,3351,3352],{},"make run"," → ",[45,3355,3356],{"href":3356,"rel":3357},"http://localhost:9000",[49],[28,3359,3361],{"className":3360},[2563,2564,43],[19,3362,3363],{},"For full setup, API docs, seeded users, and access control examples, see the GitHub README.",[28,3365],{"className":3366},[99],[92,3368,2651],{"id":2650},[285,3370,3372],{"id":3371},"route-access-control-rbac","Route + Access Control (RBAC)",[296,3374,3377],{"className":332,"code":3375,"filename":3376,"language":334,"meta":301,"style":301},"use actix_web::{web, HttpResponse};\nuse crate::middlewares::auth_middleware::{AccessControl, AccessControlCondition};\n\npub fn config(cfg: &mut web::ServiceConfig) {\n    cfg\n        // Simple role check\n        .route(\"/api/admin\", web::get().to(|| async { HttpResponse::Ok().finish() }))\n        .wrap(AccessControl::Role(\"admin\".to_string()).into_middleware())\n\n        // Role AND permission\n        .route(\"/api/editor-blog\", web::post().to(|| async { HttpResponse::Ok().finish() }))\n        .wrap(\n            AccessControlCondition::all()\n                .add(AccessControl::Role(\"editor\".to_string()))\n                .add(AccessControl::Permission(\"blog:write\".to_string()))\n                .into_middleware(),\n        );\n}\n","app/src/api/mod.rs",[23,3378,3379,3396,3425,3429,3458,3463,3468,3524,3559,3563,3568,3616,3624,3637,3665,3691,3700,3705],{"__ignoreMap":301},[305,3380,3381,3383,3386,3388,3391,3394],{"class":307,"line":308},[305,3382,342],{"class":341},[305,3384,3385],{"class":315}," actix_web",[305,3387,348],{"class":341},[305,3389,3390],{"class":311},"{web, ",[305,3392,3393],{"class":315},"HttpResponse",[305,3395,961],{"class":311},[305,3397,3398,3400,3403,3406,3408,3411,3413,3415,3418,3420,3423],{"class":307,"line":322},[305,3399,342],{"class":341},[305,3401,3402],{"class":341}," crate::",[305,3404,3405],{"class":315},"middlewares",[305,3407,348],{"class":341},[305,3409,3410],{"class":315},"auth_middleware",[305,3412,348],{"class":341},[305,3414,351],{"class":311},[305,3416,3417],{"class":315},"AccessControl",[305,3419,357],{"class":311},[305,3421,3422],{"class":315},"AccessControlCondition",[305,3424,961],{"class":311},[305,3426,3427],{"class":307,"line":387},[305,3428,384],{"emptyLinePlaceholder":383},[305,3430,3431,3434,3437,3440,3443,3445,3448,3451,3453,3456],{"class":307,"line":427},[305,3432,3433],{"class":341},"pub",[305,3435,3436],{"class":341}," fn",[305,3438,3439],{"class":315}," config",[305,3441,3442],{"class":311},"(cfg",[305,3444,1427],{"class":341},[305,3446,3447],{"class":341}," &mut",[305,3449,3450],{"class":315}," web",[305,3452,348],{"class":341},[305,3454,3455],{"class":315},"ServiceConfig",[305,3457,2013],{"class":311},[305,3459,3460],{"class":307,"line":453},[305,3461,3462],{"class":311},"    cfg\n",[305,3464,3465],{"class":307,"line":488},[305,3466,3467],{"class":581},"        // Simple role check\n",[305,3469,3470,3473,3476,3478,3481,3483,3486,3488,3490,3492,3494,3497,3499,3502,3505,3507,3509,3511,3514,3516,3518,3521],{"class":307,"line":517},[305,3471,3472],{"class":341},"        .",[305,3474,3475],{"class":315},"route",[305,3477,412],{"class":311},[305,3479,3480],{"class":328},"\"/api/admin\"",[305,3482,357],{"class":311},[305,3484,3485],{"class":315},"web",[305,3487,348],{"class":341},[305,3489,629],{"class":315},[305,3491,445],{"class":311},[305,3493,26],{"class":341},[305,3495,3496],{"class":315},"to",[305,3498,412],{"class":311},[305,3500,3501],{"class":341},"||",[305,3503,3504],{"class":341}," async",[305,3506,2029],{"class":311},[305,3508,3393],{"class":315},[305,3510,348],{"class":341},[305,3512,3513],{"class":315},"Ok",[305,3515,445],{"class":311},[305,3517,26],{"class":341},[305,3519,3520],{"class":315},"finish",[305,3522,3523],{"class":311},"() }))\n",[305,3525,3526,3528,3531,3533,3535,3537,3540,3542,3545,3547,3549,3551,3553,3556],{"class":307,"line":522},[305,3527,3472],{"class":341},[305,3529,3530],{"class":315},"wrap",[305,3532,412],{"class":311},[305,3534,3417],{"class":315},[305,3536,348],{"class":341},[305,3538,3539],{"class":315},"Role",[305,3541,412],{"class":311},[305,3543,3544],{"class":328},"\"admin\"",[305,3546,26],{"class":341},[305,3548,478],{"class":315},[305,3550,481],{"class":311},[305,3552,26],{"class":341},[305,3554,3555],{"class":315},"into_middleware",[305,3557,3558],{"class":311},"())\n",[305,3560,3561],{"class":307,"line":553},[305,3562,384],{"emptyLinePlaceholder":383},[305,3564,3565],{"class":307,"line":585},[305,3566,3567],{"class":581},"        // Role AND permission\n",[305,3569,3570,3572,3574,3576,3579,3581,3583,3585,3588,3590,3592,3594,3596,3598,3600,3602,3604,3606,3608,3610,3612,3614],{"class":307,"line":614},[305,3571,3472],{"class":341},[305,3573,3475],{"class":315},[305,3575,412],{"class":311},[305,3577,3578],{"class":328},"\"/api/editor-blog\"",[305,3580,357],{"class":311},[305,3582,3485],{"class":315},[305,3584,348],{"class":341},[305,3586,3587],{"class":315},"post",[305,3589,445],{"class":311},[305,3591,26],{"class":341},[305,3593,3496],{"class":315},[305,3595,412],{"class":311},[305,3597,3501],{"class":341},[305,3599,3504],{"class":341},[305,3601,2029],{"class":311},[305,3603,3393],{"class":315},[305,3605,348],{"class":341},[305,3607,3513],{"class":315},[305,3609,445],{"class":311},[305,3611,26],{"class":341},[305,3613,3520],{"class":315},[305,3615,3523],{"class":311},[305,3617,3618,3620,3622],{"class":307,"line":647},[305,3619,3472],{"class":341},[305,3621,3530],{"class":315},[305,3623,995],{"class":311},[305,3625,3626,3629,3631,3634],{"class":307,"line":671},[305,3627,3628],{"class":315},"            AccessControlCondition",[305,3630,348],{"class":341},[305,3632,3633],{"class":315},"all",[305,3635,3636],{"class":311},"()\n",[305,3638,3639,3642,3645,3647,3649,3651,3653,3655,3658,3660,3662],{"class":307,"line":1057},[305,3640,3641],{"class":341},"                .",[305,3643,3644],{"class":315},"add",[305,3646,412],{"class":311},[305,3648,3417],{"class":315},[305,3650,348],{"class":341},[305,3652,3539],{"class":315},[305,3654,412],{"class":311},[305,3656,3657],{"class":328},"\"editor\"",[305,3659,26],{"class":341},[305,3661,478],{"class":315},[305,3663,3664],{"class":311},"()))\n",[305,3666,3667,3669,3671,3673,3675,3677,3680,3682,3685,3687,3689],{"class":307,"line":1088},[305,3668,3641],{"class":341},[305,3670,3644],{"class":315},[305,3672,412],{"class":311},[305,3674,3417],{"class":315},[305,3676,348],{"class":341},[305,3678,3679],{"class":315},"Permission",[305,3681,412],{"class":311},[305,3683,3684],{"class":328},"\"blog:write\"",[305,3686,26],{"class":341},[305,3688,478],{"class":315},[305,3690,3664],{"class":311},[305,3692,3693,3695,3697],{"class":307,"line":1119},[305,3694,3641],{"class":341},[305,3696,3555],{"class":315},[305,3698,3699],{"class":311},"(),\n",[305,3701,3702],{"class":307,"line":1144},[305,3703,3704],{"class":311},"        );\n",[305,3706,3707],{"class":307,"line":1149},[305,3708,2090],{"class":311},[28,3710],{"className":3711},[2962],[285,3713,3715],{"id":3714},"email-verification-middleware","Email Verification Middleware",[296,3717,3720],{"className":332,"code":3718,"filename":3719,"language":334,"meta":301,"style":301},"use actix_web::{web, middleware::from_fn};\n\npub fn routes(cfg: &mut web::ServiceConfig) {\n    cfg.route(\"/api/verified-only\", web::get().to(|| async { \"ok\" }))\n        .wrap(from_fn(crate::middlewares::auth_middleware::require_email_verification));\n}\n","app/src/api/auth_middleware_usage.rs",[23,3721,3722,3740,3744,3767,3809,3836],{"__ignoreMap":301},[305,3723,3724,3726,3728,3730,3732,3735,3737],{"class":307,"line":308},[305,3725,342],{"class":341},[305,3727,3385],{"class":315},[305,3729,348],{"class":341},[305,3731,3390],{"class":311},[305,3733,3734],{"class":315},"middleware",[305,3736,348],{"class":341},[305,3738,3739],{"class":311},"from_fn};\n",[305,3741,3742],{"class":307,"line":322},[305,3743,384],{"emptyLinePlaceholder":383},[305,3745,3746,3748,3750,3753,3755,3757,3759,3761,3763,3765],{"class":307,"line":387},[305,3747,3433],{"class":341},[305,3749,3436],{"class":341},[305,3751,3752],{"class":315}," routes",[305,3754,3442],{"class":311},[305,3756,1427],{"class":341},[305,3758,3447],{"class":341},[305,3760,3450],{"class":315},[305,3762,348],{"class":341},[305,3764,3455],{"class":315},[305,3766,2013],{"class":311},[305,3768,3769,3772,3774,3776,3778,3781,3783,3785,3787,3789,3791,3793,3795,3797,3799,3801,3803,3806],{"class":307,"line":427},[305,3770,3771],{"class":311},"    cfg",[305,3773,26],{"class":341},[305,3775,3475],{"class":315},[305,3777,412],{"class":311},[305,3779,3780],{"class":328},"\"/api/verified-only\"",[305,3782,357],{"class":311},[305,3784,3485],{"class":315},[305,3786,348],{"class":341},[305,3788,629],{"class":315},[305,3790,445],{"class":311},[305,3792,26],{"class":341},[305,3794,3496],{"class":315},[305,3796,412],{"class":311},[305,3798,3501],{"class":341},[305,3800,3504],{"class":341},[305,3802,2029],{"class":311},[305,3804,3805],{"class":328},"\"ok\"",[305,3807,3808],{"class":311}," }))\n",[305,3810,3811,3813,3815,3817,3820,3822,3825,3827,3829,3831,3833],{"class":307,"line":453},[305,3812,3472],{"class":341},[305,3814,3530],{"class":315},[305,3816,412],{"class":311},[305,3818,3819],{"class":315},"from_fn",[305,3821,412],{"class":311},[305,3823,3824],{"class":341},"crate::",[305,3826,3405],{"class":315},[305,3828,348],{"class":341},[305,3830,3410],{"class":315},[305,3832,348],{"class":341},[305,3834,3835],{"class":311},"require_email_verification));\n",[305,3837,3838],{"class":307,"line":488},[305,3839,2090],{"class":311},[28,3841],{"className":3842},[2962],[285,3844,3846],{"id":3845},"seaorm-entity-user","SeaORM Entity (User)",[296,3848,3851],{"className":332,"code":3849,"filename":3850,"language":334,"meta":301,"style":301},"use sea_orm::entity::prelude::*;\n\n#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]\n#[sea_orm(table_name = \"users\")]\npub struct Model {\n    #[sea_orm(primary_key)]\n    pub id: i64,\n    pub name: String,\n    pub email: String,\n    pub password_hash: String,\n    pub email_verified_at: Option\u003CDateTimeWithTimeZone>,\n}\n\n#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]\npub enum Relation {}\n\nimpl ActiveModelBehavior for ActiveModel {}\n","entity/src/users.rs",[23,3852,3853,3875,3879,3905,3917,3929,3934,3948,3962,3975,3988,4008,4012,4016,4043,4055,4059],{"__ignoreMap":301},[305,3854,3855,3857,3860,3862,3865,3867,3870,3873],{"class":307,"line":308},[305,3856,342],{"class":341},[305,3858,3859],{"class":315}," sea_orm",[305,3861,348],{"class":341},[305,3863,3864],{"class":315},"entity",[305,3866,348],{"class":341},[305,3868,3869],{"class":315},"prelude",[305,3871,3872],{"class":341},"::*",[305,3874,424],{"class":311},[305,3876,3877],{"class":307,"line":322},[305,3878,384],{"emptyLinePlaceholder":383},[305,3880,3881,3884,3887,3889,3892,3894,3897,3899,3902],{"class":307,"line":387},[305,3882,3883],{"class":311},"#[derive(",[305,3885,3886],{"class":315},"Clone",[305,3888,357],{"class":311},[305,3890,3891],{"class":315},"Debug",[305,3893,357],{"class":311},[305,3895,3896],{"class":315},"PartialEq",[305,3898,357],{"class":311},[305,3900,3901],{"class":315},"DeriveEntityModel",[305,3903,3904],{"class":311},")]\n",[305,3906,3907,3910,3912,3915],{"class":307,"line":427},[305,3908,3909],{"class":311},"#[sea_orm(table_name ",[305,3911,396],{"class":341},[305,3913,3914],{"class":328}," \"users\"",[305,3916,3904],{"class":311},[305,3918,3919,3921,3924,3927],{"class":307,"line":453},[305,3920,3433],{"class":341},[305,3922,3923],{"class":341}," struct",[305,3925,3926],{"class":315}," Model",[305,3928,1414],{"class":311},[305,3930,3931],{"class":307,"line":488},[305,3932,3933],{"class":311},"    #[sea_orm(primary_key)]\n",[305,3935,3936,3939,3941,3943,3946],{"class":307,"line":517},[305,3937,3938],{"class":341},"    pub",[305,3940,2937],{"class":311},[305,3942,1427],{"class":341},[305,3944,3945],{"class":315}," i64",[305,3947,944],{"class":311},[305,3949,3950,3952,3955,3957,3960],{"class":307,"line":522},[305,3951,3938],{"class":341},[305,3953,3954],{"class":311}," name",[305,3956,1427],{"class":341},[305,3958,3959],{"class":315}," String",[305,3961,944],{"class":311},[305,3963,3964,3966,3969,3971,3973],{"class":307,"line":553},[305,3965,3938],{"class":341},[305,3967,3968],{"class":311}," email",[305,3970,1427],{"class":341},[305,3972,3959],{"class":315},[305,3974,944],{"class":311},[305,3976,3977,3979,3982,3984,3986],{"class":307,"line":585},[305,3978,3938],{"class":341},[305,3980,3981],{"class":311}," password_hash",[305,3983,1427],{"class":341},[305,3985,3959],{"class":315},[305,3987,944],{"class":311},[305,3989,3990,3992,3995,3997,4000,4002,4005],{"class":307,"line":614},[305,3991,3938],{"class":341},[305,3993,3994],{"class":311}," email_verified_at",[305,3996,1427],{"class":341},[305,3998,3999],{"class":315}," Option",[305,4001,2668],{"class":311},[305,4003,4004],{"class":315},"DateTimeWithTimeZone",[305,4006,4007],{"class":311},">,\n",[305,4009,4010],{"class":307,"line":647},[305,4011,2090],{"class":311},[305,4013,4014],{"class":307,"line":671},[305,4015,384],{"emptyLinePlaceholder":383},[305,4017,4018,4020,4023,4025,4027,4029,4031,4033,4036,4038,4041],{"class":307,"line":1057},[305,4019,3883],{"class":311},[305,4021,4022],{"class":315},"Copy",[305,4024,357],{"class":311},[305,4026,3886],{"class":315},[305,4028,357],{"class":311},[305,4030,3891],{"class":315},[305,4032,357],{"class":311},[305,4034,4035],{"class":315},"EnumIter",[305,4037,357],{"class":311},[305,4039,4040],{"class":315},"DeriveRelation",[305,4042,3904],{"class":311},[305,4044,4045,4047,4050,4053],{"class":307,"line":1088},[305,4046,3433],{"class":341},[305,4048,4049],{"class":341}," enum",[305,4051,4052],{"class":315}," Relation",[305,4054,2085],{"class":311},[305,4056,4057],{"class":307,"line":1119},[305,4058,384],{"emptyLinePlaceholder":383},[305,4060,4061,4064,4067,4070,4073],{"class":307,"line":1144},[305,4062,4063],{"class":341},"impl",[305,4065,4066],{"class":315}," ActiveModelBehavior",[305,4068,4069],{"class":341}," for",[305,4071,4072],{"class":315}," ActiveModel",[305,4074,2085],{"class":311},[28,4076],{"className":4077},[2962],[285,4079,4081],{"id":4080},"send-verification-email-kafka-smtp","Send Verification Email (Kafka + SMTP)",[296,4083,4086],{"className":332,"code":4084,"filename":4085,"language":334,"meta":301,"style":301},"use messaging::{AppMessageTopic, AppMessageWrapper, EmailMessage, VerifyEmailTemplate};\nuse crate::globals;\n\npub async fn send_verification_email(name: &str, email: &str, otp: &str) -> Result\u003C(), anyhow::Error> {\n    let messaging = globals::messaging::get()?;\n    let app = common::app_config::get()?;\n\n    let msg = AppMessageWrapper {\n        key: None,\n        topic: AppMessageTopic::GeneralEmail,\n        message: EmailMessage {\n            from: format!(\"{} \u003C{}>\", app.app.name, app.messaging.default_email_from),\n            to: format!(\"{} \u003C{}>\", name, email),\n            reply_to: None,\n            subject: \"Verify your email\".into(),\n            template: VerifyEmailTemplate { name: name.into(), otp: otp.into() }.into(),\n        }.into(),\n    };\n\n    messaging.read().await.send_message(msg).await?;\n    Ok(())\n}\n","app/src/services/email.rs",[23,4087,4088,4119,4128,4132,4191,4219,4246,4250,4264,4276,4293,4305,4342,4358,4369,4386,4428,4439,4444,4448,4473,4481],{"__ignoreMap":301},[305,4089,4090,4092,4095,4097,4099,4102,4104,4107,4109,4112,4114,4117],{"class":307,"line":308},[305,4091,342],{"class":341},[305,4093,4094],{"class":315}," messaging",[305,4096,348],{"class":341},[305,4098,351],{"class":311},[305,4100,4101],{"class":315},"AppMessageTopic",[305,4103,357],{"class":311},[305,4105,4106],{"class":315},"AppMessageWrapper",[305,4108,357],{"class":311},[305,4110,4111],{"class":315},"EmailMessage",[305,4113,357],{"class":311},[305,4115,4116],{"class":315},"VerifyEmailTemplate",[305,4118,961],{"class":311},[305,4120,4121,4123,4125],{"class":307,"line":322},[305,4122,342],{"class":341},[305,4124,3402],{"class":341},[305,4126,4127],{"class":311},"globals;\n",[305,4129,4130],{"class":307,"line":387},[305,4131,384],{"emptyLinePlaceholder":383},[305,4133,4134,4136,4138,4140,4143,4146,4148,4151,4154,4157,4159,4161,4163,4166,4168,4170,4172,4174,4177,4180,4183,4185,4188],{"class":307,"line":427},[305,4135,3433],{"class":341},[305,4137,3504],{"class":341},[305,4139,3436],{"class":341},[305,4141,4142],{"class":315}," send_verification_email",[305,4144,4145],{"class":311},"(name",[305,4147,1427],{"class":341},[305,4149,4150],{"class":341}," &",[305,4152,4153],{"class":315},"str",[305,4155,4156],{"class":311},", email",[305,4158,1427],{"class":341},[305,4160,4150],{"class":341},[305,4162,4153],{"class":315},[305,4164,4165],{"class":311},", otp",[305,4167,1427],{"class":341},[305,4169,4150],{"class":341},[305,4171,4153],{"class":315},[305,4173,2796],{"class":311},[305,4175,4176],{"class":341},"->",[305,4178,4179],{"class":315}," Result",[305,4181,4182],{"class":311},"\u003C(), anyhow",[305,4184,348],{"class":341},[305,4186,4187],{"class":315},"Error",[305,4189,4190],{"class":311},"> {\n",[305,4192,4193,4196,4199,4201,4204,4206,4209,4211,4213,4215,4217],{"class":307,"line":453},[305,4194,4195],{"class":341},"    let",[305,4197,4198],{"class":311}," messaging ",[305,4200,396],{"class":341},[305,4202,4203],{"class":315}," globals",[305,4205,348],{"class":341},[305,4207,4208],{"class":315},"messaging",[305,4210,348],{"class":341},[305,4212,629],{"class":315},[305,4214,445],{"class":311},[305,4216,421],{"class":341},[305,4218,424],{"class":311},[305,4220,4221,4223,4226,4228,4231,4233,4236,4238,4240,4242,4244],{"class":307,"line":488},[305,4222,4195],{"class":341},[305,4224,4225],{"class":311}," app ",[305,4227,396],{"class":341},[305,4229,4230],{"class":315}," common",[305,4232,348],{"class":341},[305,4234,4235],{"class":315},"app_config",[305,4237,348],{"class":341},[305,4239,629],{"class":315},[305,4241,445],{"class":311},[305,4243,421],{"class":341},[305,4245,424],{"class":311},[305,4247,4248],{"class":307,"line":517},[305,4249,384],{"emptyLinePlaceholder":383},[305,4251,4252,4254,4257,4259,4262],{"class":307,"line":522},[305,4253,4195],{"class":341},[305,4255,4256],{"class":311}," msg ",[305,4258,396],{"class":341},[305,4260,4261],{"class":315}," AppMessageWrapper",[305,4263,1414],{"class":311},[305,4265,4266,4269,4271,4274],{"class":307,"line":553},[305,4267,4268],{"class":311},"        key",[305,4270,1427],{"class":341},[305,4272,4273],{"class":315}," None",[305,4275,944],{"class":311},[305,4277,4278,4281,4283,4286,4288,4291],{"class":307,"line":585},[305,4279,4280],{"class":311},"        topic",[305,4282,1427],{"class":341},[305,4284,4285],{"class":315}," AppMessageTopic",[305,4287,348],{"class":341},[305,4289,4290],{"class":315},"GeneralEmail",[305,4292,944],{"class":311},[305,4294,4295,4298,4300,4303],{"class":307,"line":614},[305,4296,4297],{"class":311},"        message",[305,4299,1427],{"class":341},[305,4301,4302],{"class":315}," EmailMessage",[305,4304,1414],{"class":311},[305,4306,4307,4310,4312,4315,4317,4320,4323,4325,4328,4330,4333,4335,4337,4339],{"class":307,"line":647},[305,4308,4309],{"class":311},"            from",[305,4311,1427],{"class":341},[305,4313,4314],{"class":315}," format!",[305,4316,412],{"class":311},[305,4318,4319],{"class":328},"\"{} \u003C{}>\"",[305,4321,4322],{"class":311},", app",[305,4324,26],{"class":341},[305,4326,4327],{"class":311},"app",[305,4329,26],{"class":341},[305,4331,4332],{"class":311},"name, app",[305,4334,26],{"class":341},[305,4336,4208],{"class":311},[305,4338,26],{"class":341},[305,4340,4341],{"class":311},"default_email_from),\n",[305,4343,4344,4347,4349,4351,4353,4355],{"class":307,"line":671},[305,4345,4346],{"class":311},"            to",[305,4348,1427],{"class":341},[305,4350,4314],{"class":315},[305,4352,412],{"class":311},[305,4354,4319],{"class":328},[305,4356,4357],{"class":311},", name, email),\n",[305,4359,4360,4363,4365,4367],{"class":307,"line":1057},[305,4361,4362],{"class":311},"            reply_to",[305,4364,1427],{"class":341},[305,4366,4273],{"class":315},[305,4368,944],{"class":311},[305,4370,4371,4374,4376,4379,4381,4384],{"class":307,"line":1088},[305,4372,4373],{"class":311},"            subject",[305,4375,1427],{"class":341},[305,4377,4378],{"class":328}," \"Verify your email\"",[305,4380,26],{"class":341},[305,4382,4383],{"class":315},"into",[305,4385,3699],{"class":311},[305,4387,4388,4391,4393,4396,4399,4401,4403,4405,4407,4410,4412,4415,4417,4419,4422,4424,4426],{"class":307,"line":1119},[305,4389,4390],{"class":311},"            template",[305,4392,1427],{"class":341},[305,4394,4395],{"class":315}," VerifyEmailTemplate",[305,4397,4398],{"class":311}," { name",[305,4400,1427],{"class":341},[305,4402,3954],{"class":311},[305,4404,26],{"class":341},[305,4406,4383],{"class":315},[305,4408,4409],{"class":311},"(), otp",[305,4411,1427],{"class":341},[305,4413,4414],{"class":311}," otp",[305,4416,26],{"class":341},[305,4418,4383],{"class":315},[305,4420,4421],{"class":311},"() }",[305,4423,26],{"class":341},[305,4425,4383],{"class":315},[305,4427,3699],{"class":311},[305,4429,4430,4433,4435,4437],{"class":307,"line":1144},[305,4431,4432],{"class":311},"        }",[305,4434,26],{"class":341},[305,4436,4383],{"class":315},[305,4438,3699],{"class":311},[305,4440,4441],{"class":307,"line":1149},[305,4442,4443],{"class":311},"    };\n",[305,4445,4446],{"class":307,"line":1155},[305,4447,384],{"emptyLinePlaceholder":383},[305,4449,4450,4453,4455,4458,4460,4463,4466,4469,4471],{"class":307,"line":1186},[305,4451,4452],{"class":311},"    messaging",[305,4454,26],{"class":341},[305,4456,4457],{"class":315},"read",[305,4459,445],{"class":311},[305,4461,4462],{"class":341},".await.",[305,4464,4465],{"class":315},"send_message",[305,4467,4468],{"class":311},"(msg)",[305,4470,448],{"class":341},[305,4472,424],{"class":311},[305,4474,4475,4478],{"class":307,"line":1191},[305,4476,4477],{"class":315},"    Ok",[305,4479,4480],{"class":311},"(())\n",[305,4482,4483],{"class":307,"line":1197},[305,4484,2090],{"class":311},[28,4486],{"className":4487},[2962],[285,4489,4491],{"id":4490},"quick-curl-test","Quick cURL Test",[296,4493,4498],{"className":4494,"code":4495,"filename":4496,"language":4497,"meta":301,"style":301},"language-bash shiki shiki-themes github-dark","# Login (use seeded users; see README for emails/passwords)\ncurl -X POST http://localhost:9000/api/auth/login \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"email\":\"admin@email.com\",\"password\":\"\u003CADMIN_PASSWORD>\"}'\n","login.sh","bash",[23,4499,4500,4505,4522,4532],{"__ignoreMap":301},[305,4501,4502],{"class":307,"line":308},[305,4503,4504],{"class":581},"# Login (use seeded users; see README for emails/passwords)\n",[305,4506,4507,4510,4513,4516,4519],{"class":307,"line":322},[305,4508,4509],{"class":315},"curl",[305,4511,4512],{"class":570}," -X",[305,4514,4515],{"class":328}," POST",[305,4517,4518],{"class":328}," http://localhost:9000/api/auth/login",[305,4520,4521],{"class":570}," \\\n",[305,4523,4524,4527,4530],{"class":307,"line":387},[305,4525,4526],{"class":570},"  -H",[305,4528,4529],{"class":328}," 'Content-Type: application/json'",[305,4531,4521],{"class":570},[305,4533,4534,4537],{"class":307,"line":427},[305,4535,4536],{"class":570},"  -d",[305,4538,4539],{"class":328}," '{\"email\":\"admin@email.com\",\"password\":\"\u003CADMIN_PASSWORD>\"}'\n",[28,4541],{"className":4542},[291],[19,4544,4545,4546],{},"For full examples and more snippets, see the README: ",[45,4547,3047],{"href":3047,"rel":4548},[49],[2398,4550,4551],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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 pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":301,"searchDepth":322,"depth":322,"links":4553},[4554,4555,4556,4557],{"id":94,"depth":322,"text":95},{"id":2096,"depth":322,"text":2097},{"id":3312,"depth":322,"text":3313},{"id":2650,"depth":322,"text":2651,"children":4558},[4559,4560,4561,4562,4563],{"id":3371,"depth":387,"text":3372},{"id":3714,"depth":387,"text":3715},{"id":3845,"depth":387,"text":3846},{"id":4080,"depth":387,"text":4081},{"id":4490,"depth":387,"text":4491},"2025-06-27T00:00:00.000Z","Production-ready Rust/Actix Web REST API starter with RBAC auth, SeaORM/PostgreSQL, Kafka-based email, and Docker tooling.","/projects/actix-web-template/main.png",{},"/projects/actix-web-starter-template",{"title":3020,"description":4565},"projects/actix-web-starter-template",[2463,4572,4573,4574,4575,4576,4577],"Actix Web","SeaORM","PostgreSQL","Kafka","SMTP","RBAC","4-R3T_olS5JHQqKTWcye15P8Hi7jzB2Sh2YOj9jqwRE",{"id":4580,"title":4581,"body":4582,"date":4768,"description":4769,"extension":2455,"image_url":4626,"link":4606,"meta":4770,"navigation":383,"path":4771,"seo":4772,"stem":4773,"tags":4774,"__hash__":4778},"projects/projects/canvas-random-floating-circle.md","Canvas Random Floating Circle",{"type":7,"value":4583,"toc":4763},[4584,4588,4591,4594,4597,4600,4622,4627,4629,4635,4637,4724,4727,4729,4739],[10,4585],{"className":4586,"date":4587},[13],"2024-05-10",[16,4589,4581],{"id":4590},"canvas-random-floating-circle",[19,4592,4593],{},"Lightweight Canvas animation where circles float randomly across the screen with gentle drift and easing.",[28,4595],{"className":4596},[2487],[33,4598],{":tags":4599},"[\"JavaScript\", \"HTML Canvas\", \"Animation\", \"Pet Project\"]",[28,4601,4603,4613],{"className":4602},[39,40,41,42,43,31],[45,4604,4608,4611],{"className":4605,"href":4606,"rel":4607,"target":2499},[51,52,53,54,55],"https://dev-davexoyinbo.github.io/canvas-random-floating-circle/",[49],[19,4609,4610],{},"Live Demo",[61,4612],{"icon":63},[45,4614,4618,4620],{"className":4615,"href":4616,"rel":4617,"target":2499},[51,52,53,54,69,70,71],"https://github.com/dev-davexoyinbo/canvas-random-floating-circle",[49],[19,4619,59],{},[61,4621],{"icon":63},[2526,4623],{"alt":4624,"className":4625,"image":4626},"canvas random floating circle image",[2530,2531],"/projects/canvas-random-floating-circle/main.png",[92,4628,95],{"id":94},[2536,4630,4632],{"className":4631},[99],[19,4633,4634],{},"This experiment uses Canvas 2D APIs to draw circles that drift around the viewport with simple easing and random direction changes. It focuses on animation loops, randomness, and smooth motion.",[92,4636,2097],{"id":2096},[28,4638,4640,4661,4682,4703],{"className":4639},[2547,2548,2549,2550,2551],[2107,4641,4643,4647],{"className":4642},[2555],[285,4644,4646],{"id":4645},"canvas-rendering","Canvas Rendering",[28,4648,4650],{"className":4649},[2563,2564],[2126,4651,4652,4655,4658],{},[2129,4653,4654],{},"2D context drawing (arcs, fills)",[2129,4656,4657],{},"Clear + redraw per frame",[2129,4659,4660],{},"Responsive canvas sizing",[2107,4662,4664,4668],{"className":4663},[2555],[285,4665,4667],{"id":4666},"randomized-motion","Randomized Motion",[28,4669,4671],{"className":4670},[2563,2564],[2126,4672,4673,4676,4679],{},[2129,4674,4675],{},"Random velocities and drift",[2129,4677,4678],{},"Occasional direction changes",[2129,4680,4681],{},"Gentle easing for a calm effect",[2107,4683,4685,4689],{"className":4684},[2555],[285,4686,4688],{"id":4687},"animation-loop","Animation Loop",[28,4690,4692],{"className":4691},[2563,2564],[2126,4693,4694,4697,4700],{},[2129,4695,4696],{},"requestAnimationFrame",[2129,4698,4699],{},"Time-based updates",[2129,4701,4702],{},"Lightweight state",[2107,4704,4706,4710],{"className":4705},[2555],[285,4707,4709],{"id":4708},"code-footprint","Code Footprint",[28,4711,4713],{"className":4712},[2563,2564],[2126,4714,4715,4718,4721],{},[2129,4716,4717],{},"Minimal JS",[2129,4719,4720],{},"No dependencies",[2129,4722,4723],{},"Easy to tweak",[28,4725],{"className":4726},[99],[92,4728,2956],{"id":2955},[28,4730,4732,4735],{"className":4731},[2547,2960,2961,2550,2551,2962],[2964,4733],{"className":4734,"image":4626},[2967],[2964,4736],{"className":4737,"image":4738},[2967],"/projects/canvas-random-floating-circle/1.png",[28,4740,4742,4750,4758],{"className":4741},[39,40,41,42],[45,4743,4746,4748],{"className":4744,"href":4606,"rel":4745,"target":2499},[51,52,53,54,55],[49],[19,4747,4610],{},[61,4749],{"icon":63},[45,4751,4754,4756],{"className":4752,"href":4616,"rel":4753,"target":2499},[51,52,53,54,69,70,71],[49],[19,4755,59],{},[61,4757],{"icon":63},[45,4759,4761],{"className":4760,"href":2392},[51,52,53,54,69,70,71],[19,4762,2396],{},{"title":301,"searchDepth":322,"depth":322,"links":4764},[4765,4766,4767],{"id":94,"depth":322,"text":95},{"id":2096,"depth":322,"text":2097},{"id":2955,"depth":322,"text":2956},"2024-05-10T00:00:00.000Z","Pet project animating randomly floating circles on HTML Canvas with simple drift and easing.",{},"/projects/canvas-random-floating-circle",{"title":4581,"description":4769},"projects/canvas-random-floating-circle",[3013,4775,4776,4777],"HTML Canvas","Animation","Pet Project","KlfEtSshSyFLhZjkZX-TJW6_f-ED-2sKeGtYLkEaM0k",1775251239671]