[{"data":1,"prerenderedAt":3283},["ShallowReactive",2],{"blog-post-\u002Fblogs\u002F2026-06-22-effect-ou-comment-arreter-de-decouvrir-ses-erreurs-en-production":3,"related-\u002Fblogs\u002F2026-06-22-effect-ou-comment-arreter-de-decouvrir-ses-erreurs-en-production":3247},{"id":4,"title":5,"alt":6,"authors":7,"body":14,"date":3230,"description":3231,"extension":3232,"image":3233,"meta":3234,"navigation":460,"ogImage":3233,"path":3235,"published":460,"reviewers":3236,"seo":3241,"stem":3242,"tags":3243,"__hash__":3246},"blogs\u002Fblogs\u002F2026-06-22-effect-ou-comment-arreter-de-decouvrir-ses-erreurs-en-production\u002Findex.md","Effect, ou comment arrêter de découvrir ses erreurs en production","Illustration abstraite et futuriste sur fond bleu nuit, montrant un hexagone lumineux au centre protégeant un noyau doré. Des lignes néon, nœuds géométriques et formes translucides convergent autour de la structure, évoquant une architecture logicielle modulaire, la sûreté de typage, les dépendances et les tests automatisés.",[8],{"id":9,"name":10,"image":11,"linkedin":12,"x":13},"376f4462-cd38-8049-8778-fd9ed33a7ddc","Alex Deneuvillers",".\u002Fassets\u002Fauthor-alex-deneuvillers.webp","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Falex-deneuvillers\u002F",null,{"type":15,"value":16,"toc":3215},"minimark",[17,21,31,34,42,56,64,67,85,90,93,318,329,372,375,382,400,437,549,560,571,581,586,589,595,744,760,786,863,877,977,980,1049,1052,1056,1065,1076,1237,1246,1253,1257,1271,1290,1293,1551,1568,1627,1630,1684,1687,1690,1694,1704,1707,1767,1777,1784,1787,1922,1932,2261,2276,2279,2283,2289,2299,2310,2650,2653,2664,2682,2685,2689,2700,2988,2998,3067,3070,3074,3077,3146,3150,3153,3156,3174,3180,3183,3202,3205,3208,3211],[18,19,20],"p",{},"À mesure que les systèmes gagnent en complexité, les approches traditionnelles montrent leurs limites. Dépendances cachées, erreurs silencieuses, tests laborieux : autant de symptômes qui ralentissent l’évolution de nos applications.",[18,22,23,30],{},[24,25,29],"a",{"href":26,"rel":27},"https:\u002F\u002Fblog.hoppr.tech\u002Fblogs\u002F2026-04-08-architecture-hexagonale-en-programmation-fonctionnelle-mythe-ou-ralit",[28],"nofollow","Un précédent article l’a montré",", l’architecture hexagonale et la programmation fonctionnelle partagent une même ambition : recentrer le métier, le protéger de la technique. Ce mariage tenait aussi bien en Gleam qu’en Java.",[18,32,33],{},"Restait une question : et en TypeScript ? Le langage est partout, mais il traîne deux angles morts tenaces.",[18,35,36,37,41],{},"Une fonction qui renvoie ",[38,39,40],"code",{},"Promise\u003CTâche>"," ne dit ni qu’elle interroge une base de données, ni qu’elle peut échouer de cinq manières différentes. On le découvre en production.",[18,43,44,45,50,51,55],{},"C’est là qu’intervient ",[24,46,49],{"href":47,"rel":48},"https:\u002F\u002Feffect.website\u002Fdocs",[28],"Effect",", une bibliothèque qui fait remonter les erreurs ",[52,53,54],"em",{},"et"," les dépendances dans le système de types.",[18,57,58,59,63],{},"Pas d’empilement de « hello world » ici. Nous construirons une petite ",[60,61,62],"strong",{},"API de gestion de tâches",", fil rouge pour montrer ce qu’Effect facilite vraiment : l’architecture hexagonale et le TDD.",[18,65,66],{},"Non plus comme une discipline qu’on s’impose, mais comme le chemin de moindre résistance.",[68,69,70],"blockquote",{},[18,71,72,73,76,77,80,81,84],{},"Les exemples ciblent ",[60,74,75],{},"Effect 3.x"," (version stable courante : ",[38,78,79],{},"3.21","). Une v4 est en bêta, mais le code de production tourne aujourd’hui sur la 3.x. Épinglez ",[38,82,83],{},"effect@^3",".",[86,87,89],"h2",{"id":88},"le-problème-ce-que-typescript-ne-vous-dit-pas","Le problème : ce que TypeScript ne vous dit pas",[18,91,92],{},"Commençons par un service de tâches « normal », tel qu’on l’écrit tous les jours.",[94,95,100],"pre",{"className":96,"code":97,"language":98,"meta":99,"style":99},"language-typescript shiki shiki-themes github-dark-default","async function completeTask(id: string): Promise\u003CTask> {\n  const task = await db.tasks.findById(id)        \u002F\u002F dépendance cachée n°1\n  if (!task) throw new Error(\"not found\")         \u002F\u002F erreur invisible n°1\n  if (task.done) throw new Error(\"already done\")  \u002F\u002F erreur invisible n°2\n  const completed = { ...task, done: true }\n  await db.tasks.save(completed)                   \u002F\u002F dépendance cachée n°2\n  await mailer.send(\u002F* ... *\u002F)                     \u002F\u002F dépendance cachée n°3\n  return completed\n}\n","typescript","",[38,101,102,150,178,214,239,264,281,303,312],{"__ignoreMap":99},[103,104,107,111,114,118,122,126,129,133,136,138,141,144,147],"span",{"class":105,"line":106},"line",1,[103,108,110],{"class":109},"suJrU","async",[103,112,113],{"class":109}," function",[103,115,117],{"class":116},"sc3cj"," completeTask",[103,119,121],{"class":120},"sZEs4","(",[103,123,125],{"class":124},"sQhOw","id",[103,127,128],{"class":109},":",[103,130,132],{"class":131},"sFSAA"," string",[103,134,135],{"class":120},")",[103,137,128],{"class":109},[103,139,140],{"class":124}," Promise",[103,142,143],{"class":120},"\u003C",[103,145,146],{"class":124},"Task",[103,148,149],{"class":120},"> {\n",[103,151,153,156,159,162,165,168,171,174],{"class":105,"line":152},2,[103,154,155],{"class":109},"  const",[103,157,158],{"class":131}," task",[103,160,161],{"class":109}," =",[103,163,164],{"class":109}," await",[103,166,167],{"class":120}," db.tasks.",[103,169,170],{"class":116},"findById",[103,172,173],{"class":120},"(id)        ",[103,175,177],{"class":176},"sH3jZ","\u002F\u002F dépendance cachée n°1\n",[103,179,181,184,187,190,193,196,199,202,204,208,211],{"class":105,"line":180},3,[103,182,183],{"class":109},"  if",[103,185,186],{"class":120}," (",[103,188,189],{"class":109},"!",[103,191,192],{"class":120},"task) ",[103,194,195],{"class":109},"throw",[103,197,198],{"class":109}," new",[103,200,201],{"class":116}," Error",[103,203,121],{"class":120},[103,205,207],{"class":206},"s9uIt","\"not found\"",[103,209,210],{"class":120},")         ",[103,212,213],{"class":176},"\u002F\u002F erreur invisible n°1\n",[103,215,217,219,222,224,226,228,230,233,236],{"class":105,"line":216},4,[103,218,183],{"class":109},[103,220,221],{"class":120}," (task.done) ",[103,223,195],{"class":109},[103,225,198],{"class":109},[103,227,201],{"class":116},[103,229,121],{"class":120},[103,231,232],{"class":206},"\"already done\"",[103,234,235],{"class":120},")  ",[103,237,238],{"class":176},"\u002F\u002F erreur invisible n°2\n",[103,240,242,244,247,249,252,255,258,261],{"class":105,"line":241},5,[103,243,155],{"class":109},[103,245,246],{"class":131}," completed",[103,248,161],{"class":109},[103,250,251],{"class":120}," { ",[103,253,254],{"class":109},"...",[103,256,257],{"class":120},"task, done: ",[103,259,260],{"class":131},"true",[103,262,263],{"class":120}," }\n",[103,265,267,270,272,275,278],{"class":105,"line":266},6,[103,268,269],{"class":109},"  await",[103,271,167],{"class":120},[103,273,274],{"class":116},"save",[103,276,277],{"class":120},"(completed)                   ",[103,279,280],{"class":176},"\u002F\u002F dépendance cachée n°2\n",[103,282,284,286,289,292,294,297,300],{"class":105,"line":283},7,[103,285,269],{"class":109},[103,287,288],{"class":120}," mailer.",[103,290,291],{"class":116},"send",[103,293,121],{"class":120},[103,295,296],{"class":176},"\u002F* ... *\u002F",[103,298,299],{"class":120},")                     ",[103,301,302],{"class":176},"\u002F\u002F dépendance cachée n°3\n",[103,304,306,309],{"class":105,"line":305},8,[103,307,308],{"class":109},"  return",[103,310,311],{"class":120}," completed\n",[103,313,315],{"class":105,"line":314},9,[103,316,317],{"class":120},"}\n",[18,319,320,321,324,325,328],{},"Vous voyez le problème ? Sans doute pas tout de suite, et c’est bien là le souci. La signature ",[38,322,323],{},"(id: string) => Promise\u003CTask>"," est un ",[52,326,327],{},"mensonge par omission"," :",[330,331,332,344,351,362],"ul",{},[333,334,335,336,339,340,343],"li",{},"Elle ",[60,337,338],{},"tait"," que la fonction a besoin de db et de mailer. Et chacune de ces dépendances ",[60,341,342],{},"traîne ses propres possibilités d'échec :"," connexion DB perdue, SMTP injoignable, qui n'apparaissent ni dans la signature, ni même dans le corps de la fonction.",[333,345,346,347,350],{},"Pire, ces dépendances sont ",[60,348,349],{},"couplées en dur"," à des implémentations concrètes (import), pas à des abstractions. C'est ce couplage qui rend les tests pénibles : mock, jest.mock, monkey-patching…",[333,352,353,354,357,358,361],{},"Elle tait le fait qu’elle puisse échouer, et pourquoi. Le type de rejet d’une ",[38,355,356],{},"Promise"," est ",[38,359,360],{},"any",". Rien ne vous oblige à traiter le cas « tâche introuvable ».",[333,363,364,365,367,368,371],{},"Les ",[38,366,356],{}," sont ",[52,369,370],{},"eager"," : elles démarrent dès leur création. Impossible de décrire un calcul, de le réessayer ou de l’instrumenter sans relancer ses effets de bord.",[18,373,374],{},"Effect répond à ces trois problèmes avec un seul type.",[86,376,378,379],{"id":377},"un-type-trois-canaux-effecta-e-r","Un type, trois canaux : ",[38,380,381],{},"Effect\u003CA, E, R>",[18,383,384,385,387,388,391,392,395,396,399],{},"Un ",[38,386,49],{}," est une ",[52,389,390],{},"description immuable et paresseuse"," d’un calcul; celui-ci pouvant être synchrone comme asynchrone. C'est un plan explicite de ce qui ",[52,393,394],{},"peut"," se produire. Rien ne tourne tant que vous ne le confiez pas à un ",[52,397,398],{},"runtime"," qui sera en charge de l'exécuter.  trois canaux :",[330,401,402,411,424],{},[333,403,404,186,407,410],{},[38,405,406],{},"A",[60,408,409],{},"Success",") est la valeur produite en cas de succès ;",[333,412,413,186,416,419,420,423],{},[38,414,415],{},"E",[60,417,418],{},"Error",") regroupe les erreurs ",[52,421,422],{},"attendues"," et récupérables ;",[333,425,426,186,429,432,433,436],{},[38,427,428],{},"R",[60,430,431],{},"Requirements",") liste les services dont le programme a ",[60,434,435],{},"besoin"," pour être éxécuté.",[94,438,440],{"className":96,"code":439,"language":98,"meta":99,"style":99},"import { Effect } from \"effect\"\n\n\u002F\u002F Effect\u003CTask, TaskNotFound | TaskAlreadyCompleted, TaskRepository | Notifier>\ndeclare const completeTask: (id: string) => Effect.Effect\u003C\n  Task,                                  \u002F\u002F A : ce qu'on obtient\n  TaskNotFound | TaskAlreadyCompleted,   \u002F\u002F E : comment ça peut échouer\n  TaskRepository | Notifier              \u002F\u002F R : ce dont on a besoin\n>\n",[38,441,442,456,462,467,503,514,531,544],{"__ignoreMap":99},[103,443,444,447,450,453],{"class":105,"line":106},[103,445,446],{"class":109},"import",[103,448,449],{"class":120}," { Effect } ",[103,451,452],{"class":109},"from",[103,454,455],{"class":206}," \"effect\"\n",[103,457,458],{"class":105,"line":152},[103,459,461],{"emptyLinePlaceholder":460},true,"\n",[103,463,464],{"class":105,"line":180},[103,465,466],{"class":176},"\u002F\u002F Effect\u003CTask, TaskNotFound | TaskAlreadyCompleted, TaskRepository | Notifier>\n",[103,468,469,472,475,477,479,481,483,485,487,490,493,496,498,500],{"class":105,"line":216},[103,470,471],{"class":109},"declare",[103,473,474],{"class":109}," const",[103,476,117],{"class":116},[103,478,128],{"class":109},[103,480,186],{"class":120},[103,482,125],{"class":124},[103,484,128],{"class":109},[103,486,132],{"class":131},[103,488,489],{"class":120},") ",[103,491,492],{"class":109},"=>",[103,494,495],{"class":124}," Effect",[103,497,84],{"class":120},[103,499,49],{"class":124},[103,501,502],{"class":120},"\u003C\n",[103,504,505,508,511],{"class":105,"line":241},[103,506,507],{"class":124},"  Task",[103,509,510],{"class":120},",                                  ",[103,512,513],{"class":176},"\u002F\u002F A : ce qu'on obtient\n",[103,515,516,519,522,525,528],{"class":105,"line":266},[103,517,518],{"class":124},"  TaskNotFound",[103,520,521],{"class":109}," |",[103,523,524],{"class":124}," TaskAlreadyCompleted",[103,526,527],{"class":120},",   ",[103,529,530],{"class":176},"\u002F\u002F E : comment ça peut échouer\n",[103,532,533,536,538,541],{"class":105,"line":283},[103,534,535],{"class":124},"  TaskRepository",[103,537,521],{"class":109},[103,539,540],{"class":124}," Notifier",[103,542,543],{"class":176},"              \u002F\u002F R : ce dont on a besoin\n",[103,545,546],{"class":105,"line":305},[103,547,548],{"class":120},">\n",[18,550,551,552,555,556,559],{},"Reprenons notre ",[38,553,554],{},"completeTask",", mais cette fois en Effect. La signature dit désormais ",[52,557,558],{},"toute la vérité"," : la valeur de retour, les deux façons d’échouer, les deux dépendances.",[18,561,562,563,566,567,570],{},"Mieux : le compilateur refusera de lancer ce programme tant que ",[38,564,565],{},"TaskRepository"," et ",[38,568,569],{},"Notifier"," ne seront pas fournis. C’est cette propriété qui rend l’hexagonal et le TDD naturels (nous y reviendrons).",[18,572,573,574],{},"On pourrait résumer ainsi : ",[52,575,576,49,578],{},[38,577,384],{},[38,579,580],{}," est un processus virtuel décrit de manière déclarative, que le runtime Effect peut exécuter, suspendre, reprendre, interrompre et composer avec d'autres processus.",[582,583,585],"h3",{"id":584},"les-briques-de-base","Les briques de base",[18,587,588],{},"Avant de bâtir, posons les outils. Trois suffisent pour comprendre 90 % du code Effect que vous lirez.",[18,590,591,594],{},[60,592,593],{},"Créer un effect."," Le choix du constructeur tient à une seule question : le calcul peut-il échouer ?",[94,596,598],{"className":96,"code":597,"language":98,"meta":99,"style":99},"import { Effect } from \"effect\"\n\nEffect.succeed(42)                      \u002F\u002F Effect\u003Cnumber, never, never>\nEffect.fail(new Error(\"boom\"))          \u002F\u002F Effect\u003Cnever, Error, never>\nEffect.sync(() => console.log(\"hi\"))    \u002F\u002F effet de bord qui NE PEUT pas échouer\nEffect.tryPromise({                      \u002F\u002F Promise qui PEUT rejeter, avec erreur typée\n  try: () => fetch(url),\n  catch: (cause) => new NetworkError({ cause })\n})\n",[38,599,600,610,614,633,658,687,700,716,739],{"__ignoreMap":99},[103,601,602,604,606,608],{"class":105,"line":106},[103,603,446],{"class":109},[103,605,449],{"class":120},[103,607,452],{"class":109},[103,609,455],{"class":206},[103,611,612],{"class":105,"line":152},[103,613,461],{"emptyLinePlaceholder":460},[103,615,616,619,622,624,627,630],{"class":105,"line":180},[103,617,618],{"class":120},"Effect.",[103,620,621],{"class":116},"succeed",[103,623,121],{"class":120},[103,625,626],{"class":131},"42",[103,628,629],{"class":120},")                      ",[103,631,632],{"class":176},"\u002F\u002F Effect\u003Cnumber, never, never>\n",[103,634,635,637,640,642,645,647,649,652,655],{"class":105,"line":216},[103,636,618],{"class":120},[103,638,639],{"class":116},"fail",[103,641,121],{"class":120},[103,643,644],{"class":109},"new",[103,646,201],{"class":116},[103,648,121],{"class":120},[103,650,651],{"class":206},"\"boom\"",[103,653,654],{"class":120},"))          ",[103,656,657],{"class":176},"\u002F\u002F Effect\u003Cnever, Error, never>\n",[103,659,660,662,665,668,670,673,676,678,681,684],{"class":105,"line":241},[103,661,618],{"class":120},[103,663,664],{"class":116},"sync",[103,666,667],{"class":120},"(() ",[103,669,492],{"class":109},[103,671,672],{"class":120}," console.",[103,674,675],{"class":116},"log",[103,677,121],{"class":120},[103,679,680],{"class":206},"\"hi\"",[103,682,683],{"class":120},"))    ",[103,685,686],{"class":176},"\u002F\u002F effet de bord qui NE PEUT pas échouer\n",[103,688,689,691,694,697],{"class":105,"line":266},[103,690,618],{"class":120},[103,692,693],{"class":116},"tryPromise",[103,695,696],{"class":120},"({                      ",[103,698,699],{"class":176},"\u002F\u002F Promise qui PEUT rejeter, avec erreur typée\n",[103,701,702,705,708,710,713],{"class":105,"line":283},[103,703,704],{"class":116},"  try",[103,706,707],{"class":120},": () ",[103,709,492],{"class":109},[103,711,712],{"class":116}," fetch",[103,714,715],{"class":120},"(url),\n",[103,717,718,721,724,727,729,731,733,736],{"class":105,"line":305},[103,719,720],{"class":116},"  catch",[103,722,723],{"class":120},": (",[103,725,726],{"class":124},"cause",[103,728,489],{"class":120},[103,730,492],{"class":109},[103,732,198],{"class":109},[103,734,735],{"class":116}," NetworkError",[103,737,738],{"class":120},"({ cause })\n",[103,740,741],{"class":105,"line":314},[103,742,743],{"class":120},"})\n",[18,745,746,747,749,750,753,754,749,757,759],{},"La règle est simple : ",[38,748,664],{},"\u002F",[38,751,752],{},"promise"," quand ça ne peut pas échouer, ",[38,755,756],{},"try",[38,758,693],{}," quand ça peut.",[18,761,762,765,766,772,773,776,777,566,779,781,782,785],{},[60,763,764],{},"Composer avec"," ",[60,767,768,771],{},[38,769,770],{},"Effect.gen","****."," C’est l’équivalent de ",[38,774,775],{},"async\u002Fawait",", mais qui propage aussi ",[38,778,415],{},[38,780,428],{},". On ",[38,783,784],{},"yield*"," un effect pour « l’attendre » ; un échec court-circuite automatiquement la suite.",[94,787,789],{"className":96,"code":788,"language":98,"meta":99,"style":99},"const program = Effect.gen(function* () {\n  const amount = yield* fetchAmount       \u002F\u002F unwrap le succès\n  const rate = yield* fetchRate\n  return yield* applyDiscount(amount, rate)\n})\n",[38,790,791,815,833,847,859],{"__ignoreMap":99},[103,792,793,796,799,801,804,807,809,812],{"class":105,"line":106},[103,794,795],{"class":109},"const",[103,797,798],{"class":131}," program",[103,800,161],{"class":109},[103,802,803],{"class":120}," Effect.",[103,805,806],{"class":116},"gen",[103,808,121],{"class":120},[103,810,811],{"class":109},"function*",[103,813,814],{"class":120}," () {\n",[103,816,817,819,822,824,827,830],{"class":105,"line":152},[103,818,155],{"class":109},[103,820,821],{"class":131}," amount",[103,823,161],{"class":109},[103,825,826],{"class":109}," yield*",[103,828,829],{"class":120}," fetchAmount       ",[103,831,832],{"class":176},"\u002F\u002F unwrap le succès\n",[103,834,835,837,840,842,844],{"class":105,"line":180},[103,836,155],{"class":109},[103,838,839],{"class":131}," rate",[103,841,161],{"class":109},[103,843,826],{"class":109},[103,845,846],{"class":120}," fetchRate\n",[103,848,849,851,853,856],{"class":105,"line":216},[103,850,308],{"class":109},[103,852,826],{"class":109},[103,854,855],{"class":116}," applyDiscount",[103,857,858],{"class":120},"(amount, rate)\n",[103,860,861],{"class":105,"line":241},[103,862,743],{"class":120},[18,864,865,868,869,872,873,876],{},[60,866,867],{},"Nommer ses erreurs."," Fini le ",[38,870,871],{},"throw new Error(\"...\")"," anonyme. On déclare des erreurs de domaine, chacune avec son ",[38,874,875],{},"_tag"," discriminant.",[94,878,880],{"className":96,"code":879,"language":98,"meta":99,"style":99},"import { Data } from \"effect\"\n\nclass TaskNotFound extends Data.TaggedError(\"TaskNotFound\")\u003C{\n  readonly id: string\n}> {}\n\nclass TaskAlreadyCompleted extends Data.TaggedError(\"TaskAlreadyCompleted\")\u003C{\n  readonly id: string\n}> {}\n",[38,881,882,893,897,922,935,940,944,963,973],{"__ignoreMap":99},[103,883,884,886,889,891],{"class":105,"line":106},[103,885,446],{"class":109},[103,887,888],{"class":120}," { Data } ",[103,890,452],{"class":109},[103,892,455],{"class":206},[103,894,895],{"class":105,"line":152},[103,896,461],{"emptyLinePlaceholder":460},[103,898,899,902,905,908,911,914,916,919],{"class":105,"line":180},[103,900,901],{"class":109},"class",[103,903,904],{"class":124}," TaskNotFound",[103,906,907],{"class":109}," extends",[103,909,910],{"class":120}," Data.",[103,912,913],{"class":116},"TaggedError",[103,915,121],{"class":120},[103,917,918],{"class":206},"\"TaskNotFound\"",[103,920,921],{"class":120},")\u003C{\n",[103,923,924,927,930,932],{"class":105,"line":216},[103,925,926],{"class":109},"  readonly",[103,928,929],{"class":124}," id",[103,931,128],{"class":109},[103,933,934],{"class":131}," string\n",[103,936,937],{"class":105,"line":241},[103,938,939],{"class":120},"}> {}\n",[103,941,942],{"class":105,"line":266},[103,943,461],{"emptyLinePlaceholder":460},[103,945,946,948,950,952,954,956,958,961],{"class":105,"line":283},[103,947,901],{"class":109},[103,949,524],{"class":124},[103,951,907],{"class":109},[103,953,910],{"class":120},[103,955,913],{"class":116},[103,957,121],{"class":120},[103,959,960],{"class":206},"\"TaskAlreadyCompleted\"",[103,962,921],{"class":120},[103,964,965,967,969,971],{"class":105,"line":305},[103,966,926],{"class":109},[103,968,929],{"class":124},[103,970,128],{"class":109},[103,972,934],{"class":131},[103,974,975],{"class":105,"line":314},[103,976,939],{"class":120},[18,978,979],{},"On les traite ensuite de façon exhaustive, le compilateur tenant le décompte de ce qu’il reste à gérer :",[94,981,983],{"className":96,"code":982,"language":98,"meta":99,"style":99},"program.pipe(\n  Effect.catchTag(\"TaskNotFound\", (e) => Effect.succeed(`Pas de tâche${e.id}`))\n)\n\u002F\u002F Après ce catchTag, \"TaskNotFound\" disparaît du canal E.\n",[38,984,985,996,1039,1044],{"__ignoreMap":99},[103,986,987,990,993],{"class":105,"line":106},[103,988,989],{"class":120},"program.",[103,991,992],{"class":116},"pipe",[103,994,995],{"class":120},"(\n",[103,997,998,1001,1004,1006,1008,1011,1014,1016,1018,1020,1022,1024,1027,1029,1031,1033,1036],{"class":105,"line":152},[103,999,1000],{"class":120},"  Effect.",[103,1002,1003],{"class":116},"catchTag",[103,1005,121],{"class":120},[103,1007,918],{"class":206},[103,1009,1010],{"class":120},", (",[103,1012,1013],{"class":124},"e",[103,1015,489],{"class":120},[103,1017,492],{"class":109},[103,1019,803],{"class":120},[103,1021,621],{"class":116},[103,1023,121],{"class":120},[103,1025,1026],{"class":206},"`Pas de tâche${",[103,1028,1013],{"class":120},[103,1030,84],{"class":206},[103,1032,125],{"class":120},[103,1034,1035],{"class":206},"}`",[103,1037,1038],{"class":120},"))\n",[103,1040,1041],{"class":105,"line":180},[103,1042,1043],{"class":120},")\n",[103,1045,1046],{"class":105,"line":216},[103,1047,1048],{"class":176},"\u002F\u002F Après ce catchTag, \"TaskNotFound\" disparaît du canal E.\n",[18,1050,1051],{},"Impossible d’oublier un cas d’erreur : il reste inscrit dans le type tant que vous ne l’avez pas traité.",[86,1053,1055],{"id":1054},"linjection-de-dépendances-le-cœur-du-réacteur","L’injection de dépendances : le cœur du réacteur",[18,1057,1058,1059,324,1061,1064],{},"C’est ici qu’Effect se distingue vraiment. Le canal ",[38,1060,428],{},[52,1062,1063],{},"conteneur d’injection de dépendances vérifié à la compilation",". Pas de décorateurs, pas de réflexion, pas de magie au runtime : juste des types.",[18,1066,1067,1068,1071,1072,1075],{},"Dans l’article précédent, en Gleam, un port n’était qu’un ",[52,1069,1070],{},"type de fonction"," ; en Java, une interface. En Effect, c’est un ",[60,1073,1074],{},"Tag"," : un identifiant unique associé à une interface.",[94,1077,1079],{"className":96,"code":1078,"language":98,"meta":99,"style":99},"import { Context, Effect, Option } from \"effect\"\nimport { Task } from \"..\u002Fdomain\u002FTask.js\"\n\nexport class TaskRepository extends Context.Tag(\"TaskRepository\")\u003C\n  TaskRepository,\n  {\n    readonly findById: (id: string) => Effect.Effect\u003COption.Option\u003CTask>>\n    readonly save: (task: Task) => Effect.Effect\u003Cvoid>\n  }\n>() {}\n",[38,1080,1081,1092,1104,1108,1134,1141,1146,1190,1226,1231],{"__ignoreMap":99},[103,1082,1083,1085,1088,1090],{"class":105,"line":106},[103,1084,446],{"class":109},[103,1086,1087],{"class":120}," { Context, Effect, Option } ",[103,1089,452],{"class":109},[103,1091,455],{"class":206},[103,1093,1094,1096,1099,1101],{"class":105,"line":152},[103,1095,446],{"class":109},[103,1097,1098],{"class":120}," { Task } ",[103,1100,452],{"class":109},[103,1102,1103],{"class":206}," \"..\u002Fdomain\u002FTask.js\"\n",[103,1105,1106],{"class":105,"line":180},[103,1107,461],{"emptyLinePlaceholder":460},[103,1109,1110,1113,1116,1119,1121,1124,1126,1128,1131],{"class":105,"line":216},[103,1111,1112],{"class":109},"export",[103,1114,1115],{"class":109}," class",[103,1117,1118],{"class":124}," TaskRepository",[103,1120,907],{"class":109},[103,1122,1123],{"class":120}," Context.",[103,1125,1074],{"class":116},[103,1127,121],{"class":120},[103,1129,1130],{"class":206},"\"TaskRepository\"",[103,1132,1133],{"class":120},")\u003C\n",[103,1135,1136,1138],{"class":105,"line":241},[103,1137,535],{"class":124},[103,1139,1140],{"class":120},",\n",[103,1142,1143],{"class":105,"line":266},[103,1144,1145],{"class":120},"  {\n",[103,1147,1148,1151,1154,1156,1158,1160,1162,1164,1166,1168,1170,1172,1174,1176,1179,1181,1183,1185,1187],{"class":105,"line":283},[103,1149,1150],{"class":109},"    readonly",[103,1152,1153],{"class":116}," findById",[103,1155,128],{"class":109},[103,1157,186],{"class":120},[103,1159,125],{"class":124},[103,1161,128],{"class":109},[103,1163,132],{"class":131},[103,1165,489],{"class":120},[103,1167,492],{"class":109},[103,1169,495],{"class":124},[103,1171,84],{"class":120},[103,1173,49],{"class":124},[103,1175,143],{"class":120},[103,1177,1178],{"class":124},"Option",[103,1180,84],{"class":120},[103,1182,1178],{"class":124},[103,1184,143],{"class":120},[103,1186,146],{"class":124},[103,1188,1189],{"class":120},">>\n",[103,1191,1192,1194,1197,1199,1201,1204,1206,1209,1211,1213,1215,1217,1219,1221,1224],{"class":105,"line":305},[103,1193,1150],{"class":109},[103,1195,1196],{"class":116}," save",[103,1198,128],{"class":109},[103,1200,186],{"class":120},[103,1202,1203],{"class":124},"task",[103,1205,128],{"class":109},[103,1207,1208],{"class":124}," Task",[103,1210,489],{"class":120},[103,1212,492],{"class":109},[103,1214,495],{"class":124},[103,1216,84],{"class":120},[103,1218,49],{"class":124},[103,1220,143],{"class":120},[103,1222,1223],{"class":131},"void",[103,1225,548],{"class":120},[103,1227,1228],{"class":105,"line":314},[103,1229,1230],{"class":120},"  }\n",[103,1232,1234],{"class":105,"line":1233},10,[103,1235,1236],{"class":120},">() {}\n",[18,1238,1239,1240,1242,1243,1245],{},"Dès qu’un effet ",[38,1241,784],{}," ce tag, le service apparaît dans son canal ",[38,1244,428],{},". La dépendance n’est plus cachée : elle est inscrite dans le type.",[18,1247,1248,1249,1252],{},"Exactement comme le ",[38,1250,1251],{},"GetFinancialDataPort"," de notre exemple Gleam était un argument explicite de la fonction.",[582,1254,1256],{"id":1255},"aparté-sur-les-layers-et-le-câblage","Aparté sur les Layers et le câblage",[18,1258,1259,1260,1263,1264,1267,1268,84],{},"Un Tag décrit le ",[52,1261,1262],{},"quoi"," (l’interface). Reste le ",[52,1265,1266],{},"comment"," : la construction concrète du service. C’est le rôle des ",[60,1269,1270],{},"Layers",[18,1272,384,1273,1276,1277,765,1280,1283,1284,1286,1287,84],{},[38,1274,1275],{},"Layer\u003CROut, E, RIn>"," est une recette : il ",[52,1278,1279],{},"produit",[38,1281,1282],{},"ROut",", peut échouer pendant sa construction (",[38,1285,415],{},", par exemple une connexion à la base qui rate), et a lui-même besoin de ",[38,1288,1289],{},"RIn",[18,1291,1292],{},"Voici une implémentation en mémoire de notre repository :",[94,1294,1296],{"className":96,"code":1295,"language":98,"meta":99,"style":99},"import { Layer, Effect, Ref, Option } from \"effect\"\n\nexport const InMemoryTaskRepository = Layer.effect(\n  TaskRepository,\n  Effect.gen(function* () {\n    const ref = yield* Ref.make\u003CReadonlyArray\u003CTask>>([])\n    return TaskRepository.of({\n      findById: (id) =>\n        Ref.get(ref).pipe(\n          Effect.map((ts) => Option.fromNullable(ts.find((t) => t.id === id)))\n        ),\n      save: (task) =>\n        Ref.update(ref, (ts) => [...ts.filter((t) => t.id !== task.id), task])\n    })\n  })\n)\n",[38,1297,1298,1309,1313,1332,1337,1349,1379,1393,1407,1422,1470,1476,1490,1534,1540,1546],{"__ignoreMap":99},[103,1299,1300,1302,1305,1307],{"class":105,"line":106},[103,1301,446],{"class":109},[103,1303,1304],{"class":120}," { Layer, Effect, Ref, Option } ",[103,1306,452],{"class":109},[103,1308,455],{"class":206},[103,1310,1311],{"class":105,"line":152},[103,1312,461],{"emptyLinePlaceholder":460},[103,1314,1315,1317,1319,1322,1324,1327,1330],{"class":105,"line":180},[103,1316,1112],{"class":109},[103,1318,474],{"class":109},[103,1320,1321],{"class":131}," InMemoryTaskRepository",[103,1323,161],{"class":109},[103,1325,1326],{"class":120}," Layer.",[103,1328,1329],{"class":116},"effect",[103,1331,995],{"class":120},[103,1333,1334],{"class":105,"line":216},[103,1335,1336],{"class":120},"  TaskRepository,\n",[103,1338,1339,1341,1343,1345,1347],{"class":105,"line":241},[103,1340,1000],{"class":120},[103,1342,806],{"class":116},[103,1344,121],{"class":120},[103,1346,811],{"class":109},[103,1348,814],{"class":120},[103,1350,1351,1354,1357,1359,1361,1364,1367,1369,1372,1374,1376],{"class":105,"line":266},[103,1352,1353],{"class":109},"    const",[103,1355,1356],{"class":131}," ref",[103,1358,161],{"class":109},[103,1360,826],{"class":109},[103,1362,1363],{"class":120}," Ref.",[103,1365,1366],{"class":116},"make",[103,1368,143],{"class":120},[103,1370,1371],{"class":124},"ReadonlyArray",[103,1373,143],{"class":120},[103,1375,146],{"class":124},[103,1377,1378],{"class":120},">>([])\n",[103,1380,1381,1384,1387,1390],{"class":105,"line":283},[103,1382,1383],{"class":109},"    return",[103,1385,1386],{"class":120}," TaskRepository.",[103,1388,1389],{"class":116},"of",[103,1391,1392],{"class":120},"({\n",[103,1394,1395,1398,1400,1402,1404],{"class":105,"line":305},[103,1396,1397],{"class":116},"      findById",[103,1399,723],{"class":120},[103,1401,125],{"class":124},[103,1403,489],{"class":120},[103,1405,1406],{"class":109},"=>\n",[103,1408,1409,1412,1415,1418,1420],{"class":105,"line":314},[103,1410,1411],{"class":120},"        Ref.",[103,1413,1414],{"class":116},"get",[103,1416,1417],{"class":120},"(ref).",[103,1419,992],{"class":116},[103,1421,995],{"class":120},[103,1423,1424,1427,1430,1433,1436,1438,1440,1443,1446,1449,1452,1454,1457,1459,1461,1464,1467],{"class":105,"line":1233},[103,1425,1426],{"class":120},"          Effect.",[103,1428,1429],{"class":116},"map",[103,1431,1432],{"class":120},"((",[103,1434,1435],{"class":124},"ts",[103,1437,489],{"class":120},[103,1439,492],{"class":109},[103,1441,1442],{"class":120}," Option.",[103,1444,1445],{"class":116},"fromNullable",[103,1447,1448],{"class":120},"(ts.",[103,1450,1451],{"class":116},"find",[103,1453,1432],{"class":120},[103,1455,1456],{"class":124},"t",[103,1458,489],{"class":120},[103,1460,492],{"class":109},[103,1462,1463],{"class":120}," t.id ",[103,1465,1466],{"class":109},"===",[103,1468,1469],{"class":120}," id)))\n",[103,1471,1473],{"class":105,"line":1472},11,[103,1474,1475],{"class":120},"        ),\n",[103,1477,1479,1482,1484,1486,1488],{"class":105,"line":1478},12,[103,1480,1481],{"class":116},"      save",[103,1483,723],{"class":120},[103,1485,1203],{"class":124},[103,1487,489],{"class":120},[103,1489,1406],{"class":109},[103,1491,1493,1495,1498,1501,1503,1505,1507,1510,1512,1515,1518,1520,1522,1524,1526,1528,1531],{"class":105,"line":1492},13,[103,1494,1411],{"class":120},[103,1496,1497],{"class":116},"update",[103,1499,1500],{"class":120},"(ref, (",[103,1502,1435],{"class":124},[103,1504,489],{"class":120},[103,1506,492],{"class":109},[103,1508,1509],{"class":120}," [",[103,1511,254],{"class":109},[103,1513,1514],{"class":120},"ts.",[103,1516,1517],{"class":116},"filter",[103,1519,1432],{"class":120},[103,1521,1456],{"class":124},[103,1523,489],{"class":120},[103,1525,492],{"class":109},[103,1527,1463],{"class":120},[103,1529,1530],{"class":109},"!==",[103,1532,1533],{"class":120}," task.id), task])\n",[103,1535,1537],{"class":105,"line":1536},14,[103,1538,1539],{"class":120},"    })\n",[103,1541,1543],{"class":105,"line":1542},15,[103,1544,1545],{"class":120},"  })\n",[103,1547,1549],{"class":105,"line":1548},16,[103,1550,1043],{"class":120},[18,1552,1553,1554,1557,1558,1560,1561,1564,1565,1567],{},"Le point de bascule arrive avec ",[38,1555,1556],{},"Effect.provide"," : tant que ",[38,1559,428],{}," n’est pas ",[38,1562,1563],{},"never",", le compilateur refuse de lancer le programme. Fournir un Layer fait disparaître de la signature de l’effect le ",[38,1566,428],{}," correspondant.",[94,1569,1571],{"className":96,"code":1570,"language":98,"meta":99,"style":99},"const runnable = findTask(\"1\").pipe(\n  Effect.provide(InMemoryTaskRepository) \u002F\u002F R passe de TaskRepository à never\n)\nEffect.runPromise(runnable) \u002F\u002F ✅ compile et tourne\n",[38,1572,1573,1597,1610,1614],{"__ignoreMap":99},[103,1574,1575,1577,1580,1582,1585,1587,1590,1593,1595],{"class":105,"line":106},[103,1576,795],{"class":109},[103,1578,1579],{"class":131}," runnable",[103,1581,161],{"class":109},[103,1583,1584],{"class":116}," findTask",[103,1586,121],{"class":120},[103,1588,1589],{"class":206},"\"1\"",[103,1591,1592],{"class":120},").",[103,1594,992],{"class":116},[103,1596,995],{"class":120},[103,1598,1599,1601,1604,1607],{"class":105,"line":152},[103,1600,1000],{"class":120},[103,1602,1603],{"class":116},"provide",[103,1605,1606],{"class":120},"(InMemoryTaskRepository) ",[103,1608,1609],{"class":176},"\u002F\u002F R passe de TaskRepository à never\n",[103,1611,1612],{"class":105,"line":180},[103,1613,1043],{"class":120},[103,1615,1616,1618,1621,1624],{"class":105,"line":216},[103,1617,618],{"class":120},[103,1619,1620],{"class":116},"runPromise",[103,1622,1623],{"class":120},"(runnable) ",[103,1625,1626],{"class":176},"\u002F\u002F ✅ compile et tourne\n",[18,1628,1629],{},"Et tout l’enchaînement des dépendances se lit directement dans les types :",[94,1631,1636],{"className":1632,"code":1633,"language":1634,"meta":1635,"style":99},"language-plain shiki shiki-themes github-dark-default","ConfigLive      Layer\u003CConfig, never, never>\n   │\n   ▼\nLoggerLive      Layer\u003CLogger, never, Config>            ← a besoin de Config\n   │\n   ▼\nDatabaseLive    Layer\u003CDatabase, never, Config | Logger> ← a besoin des deux\n   │\n   ▼\nAppLive         Layer\u003CDatabase, never, never>           ← tout est câblé ✔ lançable\n","plain","text",[38,1637,1638,1643,1648,1653,1658,1662,1666,1671,1675,1679],{"__ignoreMap":99},[103,1639,1640],{"class":105,"line":106},[103,1641,1642],{},"ConfigLive      Layer\u003CConfig, never, never>\n",[103,1644,1645],{"class":105,"line":152},[103,1646,1647],{},"   │\n",[103,1649,1650],{"class":105,"line":180},[103,1651,1652],{},"   ▼\n",[103,1654,1655],{"class":105,"line":216},[103,1656,1657],{},"LoggerLive      Layer\u003CLogger, never, Config>            ← a besoin de Config\n",[103,1659,1660],{"class":105,"line":241},[103,1661,1647],{},[103,1663,1664],{"class":105,"line":266},[103,1665,1652],{},[103,1667,1668],{"class":105,"line":283},[103,1669,1670],{},"DatabaseLive    Layer\u003CDatabase, never, Config | Logger> ← a besoin des deux\n",[103,1672,1673],{"class":105,"line":305},[103,1674,1647],{},[103,1676,1677],{"class":105,"line":314},[103,1678,1652],{},[103,1680,1681],{"class":105,"line":1233},[103,1682,1683],{},"AppLive         Layer\u003CDatabase, never, never>           ← tout est câblé ✔ lançable\n",[18,1685,1686],{},"Autrement dit : vous ne pouvez pas oublier de câbler une dépendance. Si vous le faites, le code ne compile pas.",[18,1688,1689],{},"Ni un conteneur d'injection à base de réflexion (comme NestJS avec reflect-metadata par exemple), ni un câblage manuel ne vous donnent cette garantie : les premiers échouent au\nruntime, le second repose sur votre seule discipline.",[86,1691,1693],{"id":1692},"architecture-hexagonale-une-correspondance-presque-parfaite","Architecture hexagonale : une correspondance presque parfaite",[18,1695,1696,1697,1700,1701,1592],{},"Nous voilà à la thèse de l’article. En architecture hexagonale, la logique métier dépend d’interfaces abstraites (les ",[52,1698,1699],{},"ports","), et l’infrastructure fournit des implémentations concrètes (les ",[52,1702,1703],{},"adaptateurs",[18,1705,1706],{},"Avec Effect, la correspondance est quasi un pour un.",[1708,1709,1710,1723],"table",{},[1711,1712,1713],"thead",{},[1714,1715,1716,1720],"tr",{},[1717,1718,1719],"th",{},"Concept hexagonal",[1717,1721,1722],{},"Construction Effect",[1724,1725,1726,1735,1743,1751,1759],"tbody",{},[1714,1727,1728,1732],{},[1729,1730,1731],"td",{},"Port  (interface dont dépend le métier)",[1729,1733,1734],{},"un  Tag  de service",[1714,1736,1737,1740],{},[1729,1738,1739],{},"Adaptateur  (implémentation concrète)",[1729,1741,1742],{},"une  Layer  qui implémente ce Tag",[1714,1744,1745,1748],{},[1729,1746,1747],{},"Logique métier \u002F cas d’usage",[1729,1749,1750],{},"des  Effect  qui  yield*  les ports",[1714,1752,1753,1756],{},[1729,1754,1755],{},"Composition root  (câblage)",[1729,1757,1758],{},"Layer.provide  \u002F  Layer.merge , à la frontière",[1714,1760,1761,1764],{},[1729,1762,1763],{},"Adaptateur primaire (HTTP, CLI)",[1729,1765,1766],{},"handlers  @effect\u002Fplatform , point d’entrée",[18,1768,1769,1770,1773,1774,1776],{},"La propriété clé est celle qui nous tient à cœur depuis le début : ",[60,1771,1772],{},"le domaine n’importe jamais l’infrastructure."," Il ne référence que le ",[52,1775,1074],{}," du port.",[18,1778,1779,1780,1783],{},"Remplacer Postgres par une implémentation en mémoire revient à remplacer ",[52,1781,1782],{},"une seule Layer"," ; rien ne change dans le domaine.",[18,1785,1786],{},"Concrètement, on structure le projet pour matérialiser l’hexagone :",[94,1788,1790],{"className":1632,"code":1789,"language":1634,"meta":1635,"style":99},"src\u002F\n├── domain\u002F                # types purs + règles métier + erreurs de domaine\n│   ├── Task.ts\n│   └── errors.ts\n│\n├── ports\u002F                 # les interfaces (Tags) dont dépend le domaine\n│   ├── TaskRepository.ts\n│   └── Notifier.ts\n│\n├── application\u002F           # cas d'usage orchestrant les ports (intérieur de l'hexagone)\n│   └── completeTask.ts\n│\n├── adapters\u002F              # implémentations (Layers) — l'extérieur\n│   ├── persistence\u002F\n│   │   ├── PgTaskRepository.ts        # adaptateur DB réel\n│   │   └── InMemoryTaskRepository.ts  # adaptateur de test\u002Fdev\n│   │\n│   ├── notification\u002F\n│   │   ├── EmailNotifier.ts\n│   │   └── NoopNotifier.ts\n│   │\n│   └── http\u002F\n│       └── handlers.ts\n│\n└── main.ts                # composition root : seul endroit où l'on choisit les adaptateurs\n",[38,1791,1792,1797,1802,1807,1812,1817,1822,1827,1832,1836,1841,1846,1850,1855,1860,1865,1870,1876,1882,1888,1894,1899,1905,1911,1916],{"__ignoreMap":99},[103,1793,1794],{"class":105,"line":106},[103,1795,1796],{},"src\u002F\n",[103,1798,1799],{"class":105,"line":152},[103,1800,1801],{},"├── domain\u002F                # types purs + règles métier + erreurs de domaine\n",[103,1803,1804],{"class":105,"line":180},[103,1805,1806],{},"│   ├── Task.ts\n",[103,1808,1809],{"class":105,"line":216},[103,1810,1811],{},"│   └── errors.ts\n",[103,1813,1814],{"class":105,"line":241},[103,1815,1816],{},"│\n",[103,1818,1819],{"class":105,"line":266},[103,1820,1821],{},"├── ports\u002F                 # les interfaces (Tags) dont dépend le domaine\n",[103,1823,1824],{"class":105,"line":283},[103,1825,1826],{},"│   ├── TaskRepository.ts\n",[103,1828,1829],{"class":105,"line":305},[103,1830,1831],{},"│   └── Notifier.ts\n",[103,1833,1834],{"class":105,"line":314},[103,1835,1816],{},[103,1837,1838],{"class":105,"line":1233},[103,1839,1840],{},"├── application\u002F           # cas d'usage orchestrant les ports (intérieur de l'hexagone)\n",[103,1842,1843],{"class":105,"line":1472},[103,1844,1845],{},"│   └── completeTask.ts\n",[103,1847,1848],{"class":105,"line":1478},[103,1849,1816],{},[103,1851,1852],{"class":105,"line":1492},[103,1853,1854],{},"├── adapters\u002F              # implémentations (Layers) — l'extérieur\n",[103,1856,1857],{"class":105,"line":1536},[103,1858,1859],{},"│   ├── persistence\u002F\n",[103,1861,1862],{"class":105,"line":1542},[103,1863,1864],{},"│   │   ├── PgTaskRepository.ts        # adaptateur DB réel\n",[103,1866,1867],{"class":105,"line":1548},[103,1868,1869],{},"│   │   └── InMemoryTaskRepository.ts  # adaptateur de test\u002Fdev\n",[103,1871,1873],{"class":105,"line":1872},17,[103,1874,1875],{},"│   │\n",[103,1877,1879],{"class":105,"line":1878},18,[103,1880,1881],{},"│   ├── notification\u002F\n",[103,1883,1885],{"class":105,"line":1884},19,[103,1886,1887],{},"│   │   ├── EmailNotifier.ts\n",[103,1889,1891],{"class":105,"line":1890},20,[103,1892,1893],{},"│   │   └── NoopNotifier.ts\n",[103,1895,1897],{"class":105,"line":1896},21,[103,1898,1875],{},[103,1900,1902],{"class":105,"line":1901},22,[103,1903,1904],{},"│   └── http\u002F\n",[103,1906,1908],{"class":105,"line":1907},23,[103,1909,1910],{},"│       └── handlers.ts\n",[103,1912,1914],{"class":105,"line":1913},24,[103,1915,1816],{},[103,1917,1919],{"class":105,"line":1918},25,[103,1920,1921],{},"└── main.ts                # composition root : seul endroit où l'on choisit les adaptateurs\n",[18,1923,1924,1925,1927,1928,1931],{},"Le cas d’usage ",[38,1926,554],{}," devient alors de la pure orchestration de ports : l’équivalent direct de notre ",[38,1929,1930],{},"calculate_consultant_annual_packaging"," en Gleam, qui recevait ses ports en argument.",[94,1933,1935],{"className":96,"code":1934,"language":98,"meta":99,"style":99},"\u002F\u002F application\u002FcompleteTask.ts\nimport { Effect, Option } from \"effect\"\nimport { TaskRepository } from \"..\u002Fports\u002FTaskRepository.js\"\nimport { Notifier } from \"..\u002Fports\u002FNotifier.js\"\nimport { Task } from \"..\u002Fdomain\u002FTask.js\"\nimport { TaskNotFound, TaskAlreadyCompleted } from \"..\u002Fdomain\u002Ferrors.js\"\n\nexport const completeTask = (id: string) =>\n  Effect.gen(function* () {\n    const repo = yield* TaskRepository\n    const notifier = yield* Notifier\n\n    const maybe = yield* repo.findById(id)\n    const task = yield* Option.match(maybe, {\n      onNone: () => Effect.fail(new TaskNotFound({ id })),\n      onSome: Effect.succeed\n    })\n\n    if (task.done) return yield* Effect.fail(new TaskAlreadyCompleted({ id }))\n\n    const completed = new Task({ ...task, done: true })\n    yield* repo.save(completed)\n    yield* notifier.notify({ to: \"owner@example.com\", body: `Tâche${id} terminée` })\n    return completed\n  })\n\n\u002F\u002F Type inféré :\n\u002F\u002F Effect\u003CTask, TaskNotFound | TaskAlreadyCompleted, TaskRepository | Notifier>\n",[38,1936,1937,1942,1953,1965,1977,1987,1999,2003,2025,2037,2051,2065,2069,2088,2106,2128,2133,2137,2141,2166,2170,2194,2206,2235,2241,2245,2250,2256],{"__ignoreMap":99},[103,1938,1939],{"class":105,"line":106},[103,1940,1941],{"class":176},"\u002F\u002F application\u002FcompleteTask.ts\n",[103,1943,1944,1946,1949,1951],{"class":105,"line":152},[103,1945,446],{"class":109},[103,1947,1948],{"class":120}," { Effect, Option } ",[103,1950,452],{"class":109},[103,1952,455],{"class":206},[103,1954,1955,1957,1960,1962],{"class":105,"line":180},[103,1956,446],{"class":109},[103,1958,1959],{"class":120}," { TaskRepository } ",[103,1961,452],{"class":109},[103,1963,1964],{"class":206}," \"..\u002Fports\u002FTaskRepository.js\"\n",[103,1966,1967,1969,1972,1974],{"class":105,"line":216},[103,1968,446],{"class":109},[103,1970,1971],{"class":120}," { Notifier } ",[103,1973,452],{"class":109},[103,1975,1976],{"class":206}," \"..\u002Fports\u002FNotifier.js\"\n",[103,1978,1979,1981,1983,1985],{"class":105,"line":241},[103,1980,446],{"class":109},[103,1982,1098],{"class":120},[103,1984,452],{"class":109},[103,1986,1103],{"class":206},[103,1988,1989,1991,1994,1996],{"class":105,"line":266},[103,1990,446],{"class":109},[103,1992,1993],{"class":120}," { TaskNotFound, TaskAlreadyCompleted } ",[103,1995,452],{"class":109},[103,1997,1998],{"class":206}," \"..\u002Fdomain\u002Ferrors.js\"\n",[103,2000,2001],{"class":105,"line":283},[103,2002,461],{"emptyLinePlaceholder":460},[103,2004,2005,2007,2009,2011,2013,2015,2017,2019,2021,2023],{"class":105,"line":305},[103,2006,1112],{"class":109},[103,2008,474],{"class":109},[103,2010,117],{"class":116},[103,2012,161],{"class":109},[103,2014,186],{"class":120},[103,2016,125],{"class":124},[103,2018,128],{"class":109},[103,2020,132],{"class":131},[103,2022,489],{"class":120},[103,2024,1406],{"class":109},[103,2026,2027,2029,2031,2033,2035],{"class":105,"line":314},[103,2028,1000],{"class":120},[103,2030,806],{"class":116},[103,2032,121],{"class":120},[103,2034,811],{"class":109},[103,2036,814],{"class":120},[103,2038,2039,2041,2044,2046,2048],{"class":105,"line":1233},[103,2040,1353],{"class":109},[103,2042,2043],{"class":131}," repo",[103,2045,161],{"class":109},[103,2047,826],{"class":109},[103,2049,2050],{"class":120}," TaskRepository\n",[103,2052,2053,2055,2058,2060,2062],{"class":105,"line":1472},[103,2054,1353],{"class":109},[103,2056,2057],{"class":131}," notifier",[103,2059,161],{"class":109},[103,2061,826],{"class":109},[103,2063,2064],{"class":120}," Notifier\n",[103,2066,2067],{"class":105,"line":1478},[103,2068,461],{"emptyLinePlaceholder":460},[103,2070,2071,2073,2076,2078,2080,2083,2085],{"class":105,"line":1492},[103,2072,1353],{"class":109},[103,2074,2075],{"class":131}," maybe",[103,2077,161],{"class":109},[103,2079,826],{"class":109},[103,2081,2082],{"class":120}," repo.",[103,2084,170],{"class":116},[103,2086,2087],{"class":120},"(id)\n",[103,2089,2090,2092,2094,2096,2098,2100,2103],{"class":105,"line":1536},[103,2091,1353],{"class":109},[103,2093,158],{"class":131},[103,2095,161],{"class":109},[103,2097,826],{"class":109},[103,2099,1442],{"class":120},[103,2101,2102],{"class":116},"match",[103,2104,2105],{"class":120},"(maybe, {\n",[103,2107,2108,2111,2113,2115,2117,2119,2121,2123,2125],{"class":105,"line":1542},[103,2109,2110],{"class":116},"      onNone",[103,2112,707],{"class":120},[103,2114,492],{"class":109},[103,2116,803],{"class":120},[103,2118,639],{"class":116},[103,2120,121],{"class":120},[103,2122,644],{"class":109},[103,2124,904],{"class":116},[103,2126,2127],{"class":120},"({ id })),\n",[103,2129,2130],{"class":105,"line":1548},[103,2131,2132],{"class":120},"      onSome: Effect.succeed\n",[103,2134,2135],{"class":105,"line":1872},[103,2136,1539],{"class":120},[103,2138,2139],{"class":105,"line":1878},[103,2140,461],{"emptyLinePlaceholder":460},[103,2142,2143,2146,2148,2151,2153,2155,2157,2159,2161,2163],{"class":105,"line":1884},[103,2144,2145],{"class":109},"    if",[103,2147,221],{"class":120},[103,2149,2150],{"class":109},"return",[103,2152,826],{"class":109},[103,2154,803],{"class":120},[103,2156,639],{"class":116},[103,2158,121],{"class":120},[103,2160,644],{"class":109},[103,2162,524],{"class":116},[103,2164,2165],{"class":120},"({ id }))\n",[103,2167,2168],{"class":105,"line":1890},[103,2169,461],{"emptyLinePlaceholder":460},[103,2171,2172,2174,2176,2178,2180,2182,2185,2187,2189,2191],{"class":105,"line":1896},[103,2173,1353],{"class":109},[103,2175,246],{"class":131},[103,2177,161],{"class":109},[103,2179,198],{"class":109},[103,2181,1208],{"class":116},[103,2183,2184],{"class":120},"({ ",[103,2186,254],{"class":109},[103,2188,257],{"class":120},[103,2190,260],{"class":131},[103,2192,2193],{"class":120}," })\n",[103,2195,2196,2199,2201,2203],{"class":105,"line":1901},[103,2197,2198],{"class":109},"    yield*",[103,2200,2082],{"class":120},[103,2202,274],{"class":116},[103,2204,2205],{"class":120},"(completed)\n",[103,2207,2208,2210,2213,2216,2219,2222,2225,2228,2230,2233],{"class":105,"line":1907},[103,2209,2198],{"class":109},[103,2211,2212],{"class":120}," notifier.",[103,2214,2215],{"class":116},"notify",[103,2217,2218],{"class":120},"({ to: ",[103,2220,2221],{"class":206},"\"owner@example.com\"",[103,2223,2224],{"class":120},", body: ",[103,2226,2227],{"class":206},"`Tâche${",[103,2229,125],{"class":120},[103,2231,2232],{"class":206},"} terminée`",[103,2234,2193],{"class":120},[103,2236,2237,2239],{"class":105,"line":1913},[103,2238,1383],{"class":109},[103,2240,311],{"class":120},[103,2242,2243],{"class":105,"line":1918},[103,2244,1545],{"class":120},[103,2246,2248],{"class":105,"line":2247},26,[103,2249,461],{"emptyLinePlaceholder":460},[103,2251,2253],{"class":105,"line":2252},27,[103,2254,2255],{"class":176},"\u002F\u002F Type inféré :\n",[103,2257,2259],{"class":105,"line":2258},28,[103,2260,466],{"class":176},[18,2262,2263,2264,2266,2267,2270,2271,2266,2273,2275],{},"Regardez le type inféré, car c’est lui qui fait foi. Le canal ",[38,2265,428],{}," annonce ",[52,2268,2269],{},"exactement"," les deux ports ; le canal ",[38,2272,415],{},[52,2274,2269],{}," les deux erreurs métier.",[18,2277,2278],{},"Aucune fuite d’infrastructure dans le cas d’usage. La logique métier est explicite, pure et déterministe et le compilateur en est le gardien.",[86,2280,2282],{"id":2281},"tdd-tester-devient-trivial-et-sans-framework-de-mock","TDD : tester devient trivial (et sans framework de mock)",[18,2284,2285,2286,84],{},"C’est la conséquence directe du système de DI, et sans doute le bénéfice le plus concret au quotidien. Pour tester un cas d’usage, on ",[52,2287,2288],{},"fournit un Layer de test à la place de celle de production",[18,2290,2291,2292,2295,2296,2298],{},"Pas de ",[38,2293,2294],{},"jest.mock",", pas de monkey-patching : juste un autre adaptateur. Et le compilateur garantit que le fake respecte ",[52,2297,2269],{}," la même interface de port.",[18,2300,2301,2302,2305,2306,2309],{},"On installe ",[38,2303,2304],{},"@effect\u002Fvitest",", qui fournit ",[38,2307,2308],{},"it.effect"," . Il exécute l’effect en injectant un contexte de test déterministe.",[94,2311,2313],{"className":96,"code":2312,"language":98,"meta":99,"style":99},"\u002F\u002F application\u002FcompleteTask.test.ts\nimport { it, expect } from \"@effect\u002Fvitest\"\nimport { Effect, Layer } from \"effect\"\nimport { completeTask } from \".\u002FcompleteTask.js\"\nimport { InMemoryTaskRepository } from \"..\u002Fadapters\u002Fpersistence\u002FInMemoryTaskRepository.js\"\nimport { NoopNotifier } from \"..\u002Fadapters\u002Fnotification\u002FNoopNotifier.js\"\nimport { TaskRepository } from \"..\u002Fports\u002FTaskRepository.js\"\nimport { Task } from \"..\u002Fdomain\u002FTask.js\"\n\nconst TestLayer = Layer.mergeAll(InMemoryTaskRepository, NoopNotifier)\n\nit.effect(\"échoue avec TaskNotFound sur un id inconnu\", () =>\n  Effect.gen(function* () {\n    const exit = yield* completeTask(\"nope\").pipe(Effect.exit)\n    expect(exit._tag).toBe(\"Failure\")\n  }).pipe(Effect.provide(TestLayer))\n)\n\nit.effect(\"complète une tâche existante et la persiste\", () =>\n  Effect.gen(function* () {\n    const repo = yield* TaskRepository\n    yield* repo.save(new Task({ id: \"1\", title: \"écrire l'article\", done: false }))\n\n    const result = yield* completeTask(\"1\")\n\n    expect(result.done).toBe(true)\n  }).pipe(Effect.provide(TestLayer))\n)\n",[38,2314,2315,2320,2332,2343,2355,2367,2379,2389,2399,2403,2420,2424,2441,2453,2478,2496,2511,2515,2519,2534,2546,2558,2592,2596,2615,2619,2634,2646],{"__ignoreMap":99},[103,2316,2317],{"class":105,"line":106},[103,2318,2319],{"class":176},"\u002F\u002F application\u002FcompleteTask.test.ts\n",[103,2321,2322,2324,2327,2329],{"class":105,"line":152},[103,2323,446],{"class":109},[103,2325,2326],{"class":120}," { it, expect } ",[103,2328,452],{"class":109},[103,2330,2331],{"class":206}," \"@effect\u002Fvitest\"\n",[103,2333,2334,2336,2339,2341],{"class":105,"line":180},[103,2335,446],{"class":109},[103,2337,2338],{"class":120}," { Effect, Layer } ",[103,2340,452],{"class":109},[103,2342,455],{"class":206},[103,2344,2345,2347,2350,2352],{"class":105,"line":216},[103,2346,446],{"class":109},[103,2348,2349],{"class":120}," { completeTask } ",[103,2351,452],{"class":109},[103,2353,2354],{"class":206}," \".\u002FcompleteTask.js\"\n",[103,2356,2357,2359,2362,2364],{"class":105,"line":241},[103,2358,446],{"class":109},[103,2360,2361],{"class":120}," { InMemoryTaskRepository } ",[103,2363,452],{"class":109},[103,2365,2366],{"class":206}," \"..\u002Fadapters\u002Fpersistence\u002FInMemoryTaskRepository.js\"\n",[103,2368,2369,2371,2374,2376],{"class":105,"line":266},[103,2370,446],{"class":109},[103,2372,2373],{"class":120}," { NoopNotifier } ",[103,2375,452],{"class":109},[103,2377,2378],{"class":206}," \"..\u002Fadapters\u002Fnotification\u002FNoopNotifier.js\"\n",[103,2380,2381,2383,2385,2387],{"class":105,"line":283},[103,2382,446],{"class":109},[103,2384,1959],{"class":120},[103,2386,452],{"class":109},[103,2388,1964],{"class":206},[103,2390,2391,2393,2395,2397],{"class":105,"line":305},[103,2392,446],{"class":109},[103,2394,1098],{"class":120},[103,2396,452],{"class":109},[103,2398,1103],{"class":206},[103,2400,2401],{"class":105,"line":314},[103,2402,461],{"emptyLinePlaceholder":460},[103,2404,2405,2407,2410,2412,2414,2417],{"class":105,"line":1233},[103,2406,795],{"class":109},[103,2408,2409],{"class":131}," TestLayer",[103,2411,161],{"class":109},[103,2413,1326],{"class":120},[103,2415,2416],{"class":116},"mergeAll",[103,2418,2419],{"class":120},"(InMemoryTaskRepository, NoopNotifier)\n",[103,2421,2422],{"class":105,"line":1472},[103,2423,461],{"emptyLinePlaceholder":460},[103,2425,2426,2429,2431,2433,2436,2439],{"class":105,"line":1478},[103,2427,2428],{"class":120},"it.",[103,2430,1329],{"class":116},[103,2432,121],{"class":120},[103,2434,2435],{"class":206},"\"échoue avec TaskNotFound sur un id inconnu\"",[103,2437,2438],{"class":120},", () ",[103,2440,1406],{"class":109},[103,2442,2443,2445,2447,2449,2451],{"class":105,"line":1492},[103,2444,1000],{"class":120},[103,2446,806],{"class":116},[103,2448,121],{"class":120},[103,2450,811],{"class":109},[103,2452,814],{"class":120},[103,2454,2455,2457,2460,2462,2464,2466,2468,2471,2473,2475],{"class":105,"line":1536},[103,2456,1353],{"class":109},[103,2458,2459],{"class":131}," exit",[103,2461,161],{"class":109},[103,2463,826],{"class":109},[103,2465,117],{"class":116},[103,2467,121],{"class":120},[103,2469,2470],{"class":206},"\"nope\"",[103,2472,1592],{"class":120},[103,2474,992],{"class":116},[103,2476,2477],{"class":120},"(Effect.exit)\n",[103,2479,2480,2483,2486,2489,2491,2494],{"class":105,"line":1542},[103,2481,2482],{"class":116},"    expect",[103,2484,2485],{"class":120},"(exit._tag).",[103,2487,2488],{"class":116},"toBe",[103,2490,121],{"class":120},[103,2492,2493],{"class":206},"\"Failure\"",[103,2495,1043],{"class":120},[103,2497,2498,2501,2503,2506,2508],{"class":105,"line":1548},[103,2499,2500],{"class":120},"  }).",[103,2502,992],{"class":116},[103,2504,2505],{"class":120},"(Effect.",[103,2507,1603],{"class":116},[103,2509,2510],{"class":120},"(TestLayer))\n",[103,2512,2513],{"class":105,"line":1872},[103,2514,1043],{"class":120},[103,2516,2517],{"class":105,"line":1878},[103,2518,461],{"emptyLinePlaceholder":460},[103,2520,2521,2523,2525,2527,2530,2532],{"class":105,"line":1884},[103,2522,2428],{"class":120},[103,2524,1329],{"class":116},[103,2526,121],{"class":120},[103,2528,2529],{"class":206},"\"complète une tâche existante et la persiste\"",[103,2531,2438],{"class":120},[103,2533,1406],{"class":109},[103,2535,2536,2538,2540,2542,2544],{"class":105,"line":1890},[103,2537,1000],{"class":120},[103,2539,806],{"class":116},[103,2541,121],{"class":120},[103,2543,811],{"class":109},[103,2545,814],{"class":120},[103,2547,2548,2550,2552,2554,2556],{"class":105,"line":1896},[103,2549,1353],{"class":109},[103,2551,2043],{"class":131},[103,2553,161],{"class":109},[103,2555,826],{"class":109},[103,2557,2050],{"class":120},[103,2559,2560,2562,2564,2566,2568,2570,2572,2575,2577,2580,2583,2586,2589],{"class":105,"line":1901},[103,2561,2198],{"class":109},[103,2563,2082],{"class":120},[103,2565,274],{"class":116},[103,2567,121],{"class":120},[103,2569,644],{"class":109},[103,2571,1208],{"class":116},[103,2573,2574],{"class":120},"({ id: ",[103,2576,1589],{"class":206},[103,2578,2579],{"class":120},", title: ",[103,2581,2582],{"class":206},"\"écrire l'article\"",[103,2584,2585],{"class":120},", done: ",[103,2587,2588],{"class":131},"false",[103,2590,2591],{"class":120}," }))\n",[103,2593,2594],{"class":105,"line":1907},[103,2595,461],{"emptyLinePlaceholder":460},[103,2597,2598,2600,2603,2605,2607,2609,2611,2613],{"class":105,"line":1913},[103,2599,1353],{"class":109},[103,2601,2602],{"class":131}," result",[103,2604,161],{"class":109},[103,2606,826],{"class":109},[103,2608,117],{"class":116},[103,2610,121],{"class":120},[103,2612,1589],{"class":206},[103,2614,1043],{"class":120},[103,2616,2617],{"class":105,"line":1918},[103,2618,461],{"emptyLinePlaceholder":460},[103,2620,2621,2623,2626,2628,2630,2632],{"class":105,"line":2247},[103,2622,2482],{"class":116},[103,2624,2625],{"class":120},"(result.done).",[103,2627,2488],{"class":116},[103,2629,121],{"class":120},[103,2631,260],{"class":131},[103,2633,1043],{"class":120},[103,2635,2636,2638,2640,2642,2644],{"class":105,"line":2252},[103,2637,2500],{"class":120},[103,2639,992],{"class":116},[103,2641,2505],{"class":120},[103,2643,1603],{"class":116},[103,2645,2510],{"class":120},[103,2647,2648],{"class":105,"line":2258},[103,2649,1043],{"class":120},[18,2651,2652],{},"Le cycle TDD coule de source : on écrit le test, on l’exécute en rouge avec le Layer en mémoire, on rend le cas d’usage vert.",[18,2654,2655,2656,2659,2660,2663],{},"Et plus tard, lorsqu’on branchera l’adaptateur Postgres, ",[52,2657,2658],{},"le code de production restera identique",". Seule la Layer fournie change entre ",[38,2661,2662],{},"main.ts"," et le test. C’est, en une phrase, toute la promesse de l’article.",[18,2665,2666,2667,2669,2670,2673,2674,2677,2678,2681],{},"Cadeau bonus : comme ",[38,2668,2308],{}," injecte une horloge de test (",[38,2671,2672],{},"TestClock","), tout code dépendant du temps ou de timeouts, ",[38,2675,2676],{},"Schedule",", ",[38,2679,2680],{},"Effect.sleep"," peut être avancé manuellement.",[18,2683,2684],{},"Vos tests temporels deviennent déterministes et instantanés, sans la moindre attente réelle.",[86,2686,2688],{"id":2687},"du-domaine-au-monde-réel-http-et-composition-root","Du domaine au monde réel : HTTP et composition root",[18,2690,2691,2692,2695,2696,2699],{},"Reste à brancher l’extérieur. Avec ",[38,2693,2694],{},"@effect\u002Fplatform",", on décrit l’API de façon déclarative, et les erreurs typées du domaine deviennent ",[52,2697,2698],{},"naturellement"," des réponses HTTP.",[94,2701,2703],{"className":96,"code":2702,"language":98,"meta":99,"style":99},"\u002F\u002F adapters\u002Fhttp\u002Fhandlers.ts (extrait)\nimport { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiBuilder, HttpApiSchema } from \"@effect\u002Fplatform\"\nimport { Schema } from \"effect\"\nimport { Task } from \"..\u002F..\u002Fdomain\u002FTask.js\"\nimport { TaskNotFound, TaskAlreadyCompleted } from \"..\u002F..\u002Fdomain\u002Ferrors.js\"\nimport { completeTask } from \"..\u002F..\u002Fapplication\u002FcompleteTask.js\"\n\nconst idParam = HttpApiSchema.param(\"id\", Schema.String)\n\nconst tasksGroup = HttpApiGroup.make(\"tasks\").add(\n  HttpApiEndpoint.post(\"complete\")`\u002Ftasks\u002F${idParam}\u002Fcomplete`\n    .addSuccess(Task)\n    .addError(TaskNotFound)          \u002F\u002F erreur typée → réponse HTTP\n    .addError(TaskAlreadyCompleted)\n)\n\nexport const api = HttpApi.make(\"tasksApi\").add(tasksGroup)\n\nexport const TasksLive = HttpApiBuilder.group(api, \"tasks\", (handlers) =>\n  handlers.handle(\"complete\", ({ path: { id } }) => completeTask(id))\n)\n",[38,2704,2705,2710,2722,2733,2744,2755,2766,2770,2793,2797,2823,2847,2858,2871,2880,2884,2888,2916,2920,2951,2984],{"__ignoreMap":99},[103,2706,2707],{"class":105,"line":106},[103,2708,2709],{"class":176},"\u002F\u002F adapters\u002Fhttp\u002Fhandlers.ts (extrait)\n",[103,2711,2712,2714,2717,2719],{"class":105,"line":152},[103,2713,446],{"class":109},[103,2715,2716],{"class":120}," { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiBuilder, HttpApiSchema } ",[103,2718,452],{"class":109},[103,2720,2721],{"class":206}," \"@effect\u002Fplatform\"\n",[103,2723,2724,2726,2729,2731],{"class":105,"line":180},[103,2725,446],{"class":109},[103,2727,2728],{"class":120}," { Schema } ",[103,2730,452],{"class":109},[103,2732,455],{"class":206},[103,2734,2735,2737,2739,2741],{"class":105,"line":216},[103,2736,446],{"class":109},[103,2738,1098],{"class":120},[103,2740,452],{"class":109},[103,2742,2743],{"class":206}," \"..\u002F..\u002Fdomain\u002FTask.js\"\n",[103,2745,2746,2748,2750,2752],{"class":105,"line":241},[103,2747,446],{"class":109},[103,2749,1993],{"class":120},[103,2751,452],{"class":109},[103,2753,2754],{"class":206}," \"..\u002F..\u002Fdomain\u002Ferrors.js\"\n",[103,2756,2757,2759,2761,2763],{"class":105,"line":266},[103,2758,446],{"class":109},[103,2760,2349],{"class":120},[103,2762,452],{"class":109},[103,2764,2765],{"class":206}," \"..\u002F..\u002Fapplication\u002FcompleteTask.js\"\n",[103,2767,2768],{"class":105,"line":283},[103,2769,461],{"emptyLinePlaceholder":460},[103,2771,2772,2774,2777,2779,2782,2785,2787,2790],{"class":105,"line":305},[103,2773,795],{"class":109},[103,2775,2776],{"class":131}," idParam",[103,2778,161],{"class":109},[103,2780,2781],{"class":120}," HttpApiSchema.",[103,2783,2784],{"class":116},"param",[103,2786,121],{"class":120},[103,2788,2789],{"class":206},"\"id\"",[103,2791,2792],{"class":120},", Schema.String)\n",[103,2794,2795],{"class":105,"line":314},[103,2796,461],{"emptyLinePlaceholder":460},[103,2798,2799,2801,2804,2806,2809,2811,2813,2816,2818,2821],{"class":105,"line":1233},[103,2800,795],{"class":109},[103,2802,2803],{"class":131}," tasksGroup",[103,2805,161],{"class":109},[103,2807,2808],{"class":120}," HttpApiGroup.",[103,2810,1366],{"class":116},[103,2812,121],{"class":120},[103,2814,2815],{"class":206},"\"tasks\"",[103,2817,1592],{"class":120},[103,2819,2820],{"class":116},"add",[103,2822,995],{"class":120},[103,2824,2825,2828,2831,2833,2836,2838,2841,2844],{"class":105,"line":1472},[103,2826,2827],{"class":120},"  HttpApiEndpoint.",[103,2829,2830],{"class":116},"post",[103,2832,121],{"class":120},[103,2834,2835],{"class":206},"\"complete\"",[103,2837,135],{"class":120},[103,2839,2840],{"class":206},"`\u002Ftasks\u002F${",[103,2842,2843],{"class":120},"idParam",[103,2845,2846],{"class":206},"}\u002Fcomplete`\n",[103,2848,2849,2852,2855],{"class":105,"line":1478},[103,2850,2851],{"class":120},"    .",[103,2853,2854],{"class":116},"addSuccess",[103,2856,2857],{"class":120},"(Task)\n",[103,2859,2860,2862,2865,2868],{"class":105,"line":1492},[103,2861,2851],{"class":120},[103,2863,2864],{"class":116},"addError",[103,2866,2867],{"class":120},"(TaskNotFound)          ",[103,2869,2870],{"class":176},"\u002F\u002F erreur typée → réponse HTTP\n",[103,2872,2873,2875,2877],{"class":105,"line":1536},[103,2874,2851],{"class":120},[103,2876,2864],{"class":116},[103,2878,2879],{"class":120},"(TaskAlreadyCompleted)\n",[103,2881,2882],{"class":105,"line":1542},[103,2883,1043],{"class":120},[103,2885,2886],{"class":105,"line":1548},[103,2887,461],{"emptyLinePlaceholder":460},[103,2889,2890,2892,2894,2897,2899,2902,2904,2906,2909,2911,2913],{"class":105,"line":1872},[103,2891,1112],{"class":109},[103,2893,474],{"class":109},[103,2895,2896],{"class":131}," api",[103,2898,161],{"class":109},[103,2900,2901],{"class":120}," HttpApi.",[103,2903,1366],{"class":116},[103,2905,121],{"class":120},[103,2907,2908],{"class":206},"\"tasksApi\"",[103,2910,1592],{"class":120},[103,2912,2820],{"class":116},[103,2914,2915],{"class":120},"(tasksGroup)\n",[103,2917,2918],{"class":105,"line":1878},[103,2919,461],{"emptyLinePlaceholder":460},[103,2921,2922,2924,2926,2929,2931,2934,2937,2940,2942,2944,2947,2949],{"class":105,"line":1884},[103,2923,1112],{"class":109},[103,2925,474],{"class":109},[103,2927,2928],{"class":131}," TasksLive",[103,2930,161],{"class":109},[103,2932,2933],{"class":120}," HttpApiBuilder.",[103,2935,2936],{"class":116},"group",[103,2938,2939],{"class":120},"(api, ",[103,2941,2815],{"class":206},[103,2943,1010],{"class":120},[103,2945,2946],{"class":124},"handlers",[103,2948,489],{"class":120},[103,2950,1406],{"class":109},[103,2952,2953,2956,2959,2961,2963,2966,2969,2972,2974,2977,2979,2981],{"class":105,"line":1890},[103,2954,2955],{"class":120},"  handlers.",[103,2957,2958],{"class":116},"handle",[103,2960,121],{"class":120},[103,2962,2835],{"class":206},[103,2964,2965],{"class":120},", ({ ",[103,2967,2968],{"class":124},"path",[103,2970,2971],{"class":120},": { ",[103,2973,125],{"class":124},[103,2975,2976],{"class":120}," } }) ",[103,2978,492],{"class":109},[103,2980,117],{"class":116},[103,2982,2983],{"class":120},"(id))\n",[103,2985,2986],{"class":105,"line":1896},[103,2987,1043],{"class":120},[18,2989,2990,2991,2993,2994,2997],{},"Tout converge dans ",[38,2992,2662],{}," aka le ",[52,2995,2996],{},"composition root",", le seul fichier où l’on décide quels adaptateurs concrets utiliser.",[94,2999,3001],{"className":96,"code":3000,"language":98,"meta":99,"style":99},"\u002F\u002F main.ts (extrait)\nconst ApiLive = HttpApiBuilder.api(api).pipe(\n  Layer.provide(TasksLive),\n  Layer.provide(PgTaskRepository),  \u002F\u002F ← l'adaptateur RÉEL est branché ici\n  Layer.provide(EmailNotifier)      \u002F\u002F ← devient InMemory\u002FNoop dans les tests\n)\n",[38,3002,3003,3008,3029,3039,3051,3063],{"__ignoreMap":99},[103,3004,3005],{"class":105,"line":106},[103,3006,3007],{"class":176},"\u002F\u002F main.ts (extrait)\n",[103,3009,3010,3012,3015,3017,3019,3022,3025,3027],{"class":105,"line":152},[103,3011,795],{"class":109},[103,3013,3014],{"class":131}," ApiLive",[103,3016,161],{"class":109},[103,3018,2933],{"class":120},[103,3020,3021],{"class":116},"api",[103,3023,3024],{"class":120},"(api).",[103,3026,992],{"class":116},[103,3028,995],{"class":120},[103,3030,3031,3034,3036],{"class":105,"line":180},[103,3032,3033],{"class":120},"  Layer.",[103,3035,1603],{"class":116},[103,3037,3038],{"class":120},"(TasksLive),\n",[103,3040,3041,3043,3045,3048],{"class":105,"line":216},[103,3042,3033],{"class":120},[103,3044,1603],{"class":116},[103,3046,3047],{"class":120},"(PgTaskRepository),  ",[103,3049,3050],{"class":176},"\u002F\u002F ← l'adaptateur RÉEL est branché ici\n",[103,3052,3053,3055,3057,3060],{"class":105,"line":241},[103,3054,3033],{"class":120},[103,3056,1603],{"class":116},[103,3058,3059],{"class":120},"(EmailNotifier)      ",[103,3061,3062],{"class":176},"\u002F\u002F ← devient InMemory\u002FNoop dans les tests\n",[103,3064,3065],{"class":105,"line":266},[103,3066,1043],{"class":120},[18,3068,3069],{},"Passer de la production aux tests, ou de Postgres à un SQLite local, revient à changer une ligne ici. Jamais dans le domaine.",[86,3071,3073],{"id":3072},"pour-aller-plus-loin","Pour aller plus loin",[18,3075,3076],{},"Effect ne s’arrête pas aux erreurs et à la DI. Le même type unifié vous offre, sans bibliothèque tierce :",[330,3078,3079,3085,3097,3112,3122,3136],{},[333,3080,3081,3084],{},[60,3082,3083],{},"Schema"," - validation, parsing et (dé)sérialisation réversibles, à la fois types TypeScript et validateurs au runtime ;",[333,3086,3087,3092,3093,3096],{},[60,3088,3089],{},[38,3090,3091],{},"Effect.Config"," - configuration typée et testable (",[38,3094,3095],{},"Config.redacted"," masque les secrets), surchargeable dans les tests ;",[333,3098,3099,765,3104,765,3106,3111],{},[60,3100,3101],{},[38,3102,3103],{},"Scope",[60,3105,749],{},[60,3107,3108],{},[38,3109,3110],{},"acquireRelease"," - gestion de ressources avec libération garantie, même en cas d’échec ou d’interruption ;",[333,3113,3114,3117,3118,3121],{},[60,3115,3116],{},"Concurrence structurée"," - ",[38,3119,3120],{},"Effect.all(effects, { concurrency: 2 })",", fibers, nettoyage automatique ;",[333,3123,3124,765,3128,765,3130,3135],{},[60,3125,3126],{},[38,3127,2676],{},[60,3129,749],{},[60,3131,3132],{},[38,3133,3134],{},"Effect.retry"," - système de retry composables (backoff exponentiel, intervalle fixe) ;",[333,3137,3138,3141,3142,3145],{},[60,3139,3140],{},"Observabilité"," - logs structurés, métriques et traces (",[38,3143,3144],{},"Effect.withSpan",", OpenTelemetry) comme services par défaut.",[86,3147,3149],{"id":3148},"conclusion","Conclusion",[18,3151,3152],{},"Nous l’avions établi pour Gleam et Java : ce n’est pas le paradigme qui fait l’architecture, c’est la place qu’on donne au métier.",[18,3154,3155],{},"Effect prolonge ce constat jusqu’en TypeScript, et y ajoute quelque chose de précieux : le compilateur cesse d’être un correcteur orthographique pour devenir le gardien de votre architecture.",[330,3157,3158,3168,3171],{},[333,3159,3160,3161,3164,3165,3167],{},"Les erreurs et les dépendances deviennent ",[52,3162,3163],{},"visibles"," dans la signature ",[38,3166,381],{}," , rien ne peut être oublié.",[333,3169,3170],{},"Un port est un Tag, un adaptateur un Layer : l’architecture hexagonale tombe naturellement.",[333,3172,3173],{},"On teste en fournissant une Layer en mémoire, sans le moindre framework de mock, avec un code de production strictement identique.",[18,3175,3176,3177,3179],{},"Autrement dit : ce n’est pas parce qu’on écrit du TypeScript avec des ",[38,3178,356],{}," que l’on est condamné aux dépendances cachées et aux erreurs silencieuses.",[18,3181,3182],{},"Avec Effect, le métier reste au centre, et l’hexagonal comme le TDD cessent d’être une discipline pour devenir la voie de moindre effort. Le revers de la médaille étant qu’Effect ne reste pas à la frontière, il devra être présent dans toutes les couches de l’hexagone y compris le domaine, qui importe Effect et n'est plus fait de fonctions « pures » au sens strict. La direction des dépendances hexagonale tient (le cœur ne connaît que des ports), mais l'engagement est total : retirer Effect, c'est tout réécrire.",[18,3184,3185,3186,566,3188,3190,3191,3195,3196,3199,3200,84],{},"Notre conseil pour commencer : installez ",[38,3187,83],{},[38,3189,2304],{},", parcourez la ",[24,3192,3194],{"href":47,"rel":3193},[28],"documentation officielle",", puis réécrivez ",[52,3197,3198],{},"une seule"," fonction de votre codebase en ",[38,3201,770],{},[18,3203,3204],{},"Faites remonter ses erreurs et ses dépendances dans le type, et observez ce que votre compilateur avait à vous dire depuis le début.",[18,3206,3207],{},"Reste une question légitime : à force de repousser les limites de TypeScript jusqu'à en faire, via les fonctions génératrices, un système d'effets à part entière on finit par écrire des fichiers .ts qui ne ressemblent plus tout à fait à du TypeScript. À partir de quand vaudrait-il mieux changer de langage ?",[18,3209,3210],{},"Si je devais donner une première approche, ce serait presque jamais. On ne choisit pas toujours son runtime, ni son écosystème, ni son équipe, et Effect amène justement le système d'effects là où l'on travaille déjà, sans quitter npm ni tsc. Mais si vous démarrez de zéro, libre de tout, et sans TypeScript comme prérequis, la question mérite d'être posée.",[3212,3213,3214],"style",{},"html pre.shiki code .suJrU, html code.shiki .suJrU{--shiki-default:#FF7B72}html pre.shiki code .sc3cj, html code.shiki .sc3cj{--shiki-default:#D2A8FF}html pre.shiki code .sZEs4, html code.shiki .sZEs4{--shiki-default:#E6EDF3}html pre.shiki code .sQhOw, html code.shiki .sQhOw{--shiki-default:#FFA657}html pre.shiki code .sFSAA, html code.shiki .sFSAA{--shiki-default:#79C0FF}html pre.shiki code .sH3jZ, html code.shiki .sH3jZ{--shiki-default:#8B949E}html pre.shiki code .s9uIt, html code.shiki .s9uIt{--shiki-default:#A5D6FF}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":99,"searchDepth":152,"depth":152,"links":3216},[3217,3218,3222,3225,3226,3227,3228,3229],{"id":88,"depth":152,"text":89},{"id":377,"depth":152,"text":3219,"children":3220},"Un type, trois canaux : Effect\u003CA, E, R>",[3221],{"id":584,"depth":180,"text":585},{"id":1054,"depth":152,"text":1055,"children":3223},[3224],{"id":1255,"depth":180,"text":1256},{"id":1692,"depth":152,"text":1693},{"id":2281,"depth":152,"text":2282},{"id":2687,"depth":152,"text":2688},{"id":3072,"depth":152,"text":3073},{"id":3148,"depth":152,"text":3149},"2026-06-22T09:39:11.503Z","À mesure que les systèmes gagnent en complexité, les approches traditionnelles montrent leurs limites. Dépendances cachées, erreurs silencieuses, tests laborieux : autant de symptômes qui ralentissent","md",".\u002Fassets\u002Fcover-image.webp",{},"\u002Fblogs\u002F2026-06-22-effect-ou-comment-arreter-de-decouvrir-ses-erreurs-en-production",[3237],{"id":3238,"name":3239,"image":3240,"linkedin":13,"x":13},"188f4462-cd38-80d5-b9e6-ec28a94d11e5","Bastien Dufour","\u002Fdefault-author-image.webp",{"title":5,"description":3231},"blogs\u002F2026-06-22-effect-ou-comment-arreter-de-decouvrir-ses-erreurs-en-production\u002Findex",[98,3244,3245],"architecture","craft","L3jAoxBIV5wsRw2M1-j7G6xWAP8Mclipn8HzCrj5CXM",[3248,3259,3266,3275],{"path":3249,"title":3250,"image":3251,"date":3252,"tags":3253},"\u002Fblogs\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide","Du prototype à la prod : ce qu'on ne te dit pas sur la construction d'une solution IA solide","\u002Fcontent-assets\u002F2026-06-09-du-prototype-a-la-prod-ce-quon-ne-te-dit-pas-sur-la-construction-dune-solution-ia-solide\u002Fassets\u002Fcover-image.webp","9 juin 2026",[3254,3255,3256,3257,98,3258,3244,3245],"ia","test","testing","observabilité","java",{"path":3260,"title":3261,"image":3262,"date":3263,"tags":3264},"\u002Fblogs\u002F2026-06-22-clean-architecture-avec-react-remettre-des-frontieres-dans-un-frontend-qui-grandit","Clean Architecture avec React : remettre des frontières dans un frontend qui grandit","\u002Fcontent-assets\u002F2026-06-22-clean-architecture-avec-react-remettre-des-frontieres-dans-un-frontend-qui-grandit\u002Fassets\u002Fcover-image.webp","22 juin 2026",[3245,3244,3265],"front-end",{"path":3267,"title":3268,"image":3269,"date":3270,"tags":3271},"\u002Fblogs\u002F2026-05-27-platform-engineering-lart-de-dompter-lentropie-du-cloud","Platform Engineering : L’art de dompter l’entropie du cloud","\u002Fcontent-assets\u002F2026-05-27-platform-engineering-lart-de-dompter-lentropie-du-cloud\u002Fassets\u002Fcover-image.webp","27 mai 2026",[3272,3273,3245,3257,3274,3244],"plateform engineering","cloud","devops",{"path":3276,"title":3277,"image":3278,"date":3279,"tags":3280},"\u002Fblogs\u002F2025-01-07-tlchargement-et-gestion-de-packages-npm-avec-bun-et-axios","Téléchargement et gestion de packages npm avec Bun et Axios","\u002Fcontent-assets\u002F2025-01-07-tlchargement-et-gestion-de-packages-npm-avec-bun-et-axios\u002Fassets\u002Fcover-image.webp","7 janvier 2025",[98,3245,3281,3282],"fixentropy.io","dragee.io",1782199784193]