[{"data":1,"prerenderedAt":3147},["ShallowReactive",2],{"author-romain-koenig":3,"author-articles-romain-koenig":22,"authors":2831},{"id":4,"title":5,"body":6,"description":10,"extension":13,"meta":14,"name":15,"navigation":16,"path":17,"readingTime":18,"seo":19,"stem":20,"__hash__":21},"authors\u002Fauthors\u002Fromain-koenig.md","Co-funder & Head of innovation",{"type":7,"value":8,"toc":9},"minimark",[],{"title":10,"searchDepth":11,"depth":11,"links":12},"",2,[],"md",{},"Romain Koenig",true,"\u002Fauthors\u002Fromain-koenig",1,{"title":5,"description":10},"authors\u002Fromain-koenig","uyS8--eG2_ezyqRABcJnMJmQKKuSArhPWd14aUvFeEw",[23,2483],{"id":24,"title":25,"author":26,"body":27,"date":2473,"description":2474,"extension":13,"lang":2475,"meta":2476,"navigation":16,"path":2477,"published":16,"readingTime":434,"seo":2478,"stem":2479,"tags":2480,"__hash__":2482},"articles\u002Farticles\u002F2022-09-01-application-events.md","Application Events","romain-koenig",{"type":7,"value":28,"toc":2459},[29,37,49,59,63,74,77,99,102,111,114,120,123,128,296,299,302,309,502,505,512,527,533,542,545,633,636,639,650,653,662,668,674,677,683,889,892,990,993,996,1007,1016,1019,1025,1028,1035,1044,1185,1200,1214,1217,1291,1294,1360,1363,1366,1375,1381,1388,1391,1394,1400,1415,1418,1428,1439,1445,2068,2074,2079,2082,2088,2091,2264,2276,2282,2285,2432,2435,2438,2444,2447,2450,2455],[30,31,33],"h2",{"id":32},"introduction-des-événements-métiers-chez-indy",[34,35,36],"strong",{},"Introduction des événements métiers chez Indy",[38,39,40,41,45,46],"p",{},"Tag : ",[42,43,44],"code",{},"scalabilité",", ",[42,47,48],{},"architecture",[38,50,51,52,55,56],{},"Authors : ",[42,53,54],{},"Koenig R."," avec l’aide de la team ",[42,57,58],{},"Core",[30,60,62],{"id":61},"termes-employés","Termes employés",[38,64,65,66,73],{},"Qu’entendons-nous par événement métier chez ",[67,68,72],"a",{"href":69,"rel":70},"https:\u002F\u002Fwww.indy.fr\u002F",[71],"nofollow","Indy"," ? Dans le cadre d'Indy et\nde sa comptabilité je vais donner des exemples qui peuvent vous parler, histoire de bien illustrer\nla chose :",[38,75,76],{},"Nous aurons un événement métier lorsque par exemple un utilisateur...",[78,79,80,84,87,90,93,96],"ul",{},[81,82,83],"li",{},"termine son onboarding",[81,85,86],{},"s'abonne",[81,88,89],{},"ajoute son compte bancaire",[81,91,92],{},"clôture sa liasse fiscale",[81,94,95],{},"ajoute une transaction",[81,97,98],{},"change la catégorie d’une transaction",[38,100,101],{},"etc...",[38,103,104,105,110],{},"Lorsque ces événements arrivent, on veut pouvoir exécuter du code qui n’est pas relatif au cœur du\nproduit. J’entends par là, par exemple, que lorsqu’un utilisateur ajoute une transaction, on exécute\nbien le code métier qui ajoute une transaction dans l’application : C’est le produit. Mais on veut\naussi exécuter du code qui n’a rien à voir avec ce cœur de produit et ça peut être assez divers :\nEnvoyer un email ou une notification, mettre à jour un\n",[67,106,109],{"href":107,"rel":108},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCustomer_relationship_management",[71],"CRM",", envoyer une requête à un\nmicroservice.",[38,112,113],{},"Ce code lui est nécessaire au bon fonctionnement de l'entreprise, mais le produit et l’utilisateur,\neux, s’en fichent. Ainsi il serait dommage que l’utilisateur final soit pénalisé par du code destiné\naux équipes d’Indy. Que ce soit en termes d'erreur, bugs ou de délais à cause d'un élément exogène.",[30,115,117],{"id":116},"exemples-de-problèmes-avant",[34,118,119],{},"Exemples de problèmes “Avant”",[38,121,122],{},"Pour illustrer un peu plus loin les problèmes qui ont pu nous mener à cette réflexion, je propose de\nvoir des exemples sur lesquels nous nous sommes cassés un peu les dents :",[38,124,125],{},[34,126,127],{},"Exemples :",[129,130,134],"pre",{"className":131,"code":132,"language":133,"meta":10,"style":10},"language-ts shiki shiki-themes github-light github-dark","import serviceX from 'serviceX';\nimport serviceY from 'serviceY';\nimport CRM1 from 'CRM1';\n\nfunction finalizeOnboarding(...) {\n    const user = await saveUser(); \u002F\u002F do the interesting stuff for the client\n    await serviceX.initConfigurationServiceX();\n    await serviceY.initConfigurationServiceY();\n    \u002F\u002F [...]\n    await serviceX.updateCRM1(); \u002F\u002F That is our stuff\n    return user;\n    }\n","ts",[42,135,136,158,172,187,193,206,232,247,260,266,281,290],{"__ignoreMap":10},[137,138,140,144,148,151,155],"span",{"class":139,"line":18},"line",[137,141,143],{"class":142},"szBVR","import",[137,145,147],{"class":146},"sVt8B"," serviceX ",[137,149,150],{"class":142},"from",[137,152,154],{"class":153},"sZZnC"," 'serviceX'",[137,156,157],{"class":146},";\n",[137,159,160,162,165,167,170],{"class":139,"line":11},[137,161,143],{"class":142},[137,163,164],{"class":146}," serviceY ",[137,166,150],{"class":142},[137,168,169],{"class":153}," 'serviceY'",[137,171,157],{"class":146},[137,173,175,177,180,182,185],{"class":139,"line":174},3,[137,176,143],{"class":142},[137,178,179],{"class":146}," CRM1 ",[137,181,150],{"class":142},[137,183,184],{"class":153}," 'CRM1'",[137,186,157],{"class":146},[137,188,190],{"class":139,"line":189},4,[137,191,192],{"emptyLinePlaceholder":16},"\n",[137,194,196,199,203],{"class":139,"line":195},5,[137,197,198],{"class":142},"function",[137,200,202],{"class":201},"sScJk"," finalizeOnboarding",[137,204,205],{"class":146},"(...) {\n",[137,207,209,212,216,219,222,225,228],{"class":139,"line":208},6,[137,210,211],{"class":142},"    const",[137,213,215],{"class":214},"sj4cs"," user",[137,217,218],{"class":142}," =",[137,220,221],{"class":142}," await",[137,223,224],{"class":201}," saveUser",[137,226,227],{"class":146},"(); ",[137,229,231],{"class":230},"sJ8bj","\u002F\u002F do the interesting stuff for the client\n",[137,233,235,238,241,244],{"class":139,"line":234},7,[137,236,237],{"class":142},"    await",[137,239,240],{"class":146}," serviceX.",[137,242,243],{"class":201},"initConfigurationServiceX",[137,245,246],{"class":146},"();\n",[137,248,250,252,255,258],{"class":139,"line":249},8,[137,251,237],{"class":142},[137,253,254],{"class":146}," serviceY.",[137,256,257],{"class":201},"initConfigurationServiceY",[137,259,246],{"class":146},[137,261,263],{"class":139,"line":262},9,[137,264,265],{"class":230},"    \u002F\u002F [...]\n",[137,267,269,271,273,276,278],{"class":139,"line":268},10,[137,270,237],{"class":142},[137,272,240],{"class":146},[137,274,275],{"class":201},"updateCRM1",[137,277,227],{"class":146},[137,279,280],{"class":230},"\u002F\u002F That is our stuff\n",[137,282,284,287],{"class":139,"line":283},11,[137,285,286],{"class":142},"    return",[137,288,289],{"class":146}," user;\n",[137,291,293],{"class":139,"line":292},12,[137,294,295],{"class":146},"    }\n",[38,297,298],{},"Les trois dernières fonctions sont indépendantes, mais elles doivent être exécutées lors de la\nfinalisation de l'onboarding.",[38,300,301],{},"Le premier problème est qu'une erreur dans une de ces fonctions peut entraîner la non exécution des\nsuivantes, qui pourtant ne sont pas liées (elles n'attendent pas le retour des précédentes) et\nlaisser le code et la data dans un état incertain. Dans le pire des cas : On renvoie même une erreur\nà l’utilisateur qui est bloqué dans son onboarding.",[38,303,304,305,308],{},"Alors on peut répondre à ce problème en ajoutant des ",[42,306,307],{},"try catch"," et c'est ce que nous avons fait :",[129,310,312],{"className":131,"code":311,"language":133,"meta":10,"style":10},"import serviceX from 'serviceX';\nimport serviceY from 'serviceY';\nimport CRM1 from 'CRM1';\n\nfunction finalizeOnboarding(...) {\n    const businessResult = await myBusinessFunction();\n    try {\n        await initConfigurationServiceX();\n    } catch (error) {\n        \u002F\u002F do something... maybe\n    }\n    try {\n        await initConfigurationServiceY();\n    } catch (error) {\n        \u002F\u002F do something... maybe\n    }\n    try {\n        await updateCRM1();\n    } catch (error) {\n        \u002F\u002F do something... maybe it depends of the CRM\n    }\n    return businessResult;\n}\n",[42,313,314,326,338,350,354,362,378,386,396,407,412,416,422,432,441,446,451,458,468,477,483,488,496],{"__ignoreMap":10},[137,315,316,318,320,322,324],{"class":139,"line":18},[137,317,143],{"class":142},[137,319,147],{"class":146},[137,321,150],{"class":142},[137,323,154],{"class":153},[137,325,157],{"class":146},[137,327,328,330,332,334,336],{"class":139,"line":11},[137,329,143],{"class":142},[137,331,164],{"class":146},[137,333,150],{"class":142},[137,335,169],{"class":153},[137,337,157],{"class":146},[137,339,340,342,344,346,348],{"class":139,"line":174},[137,341,143],{"class":142},[137,343,179],{"class":146},[137,345,150],{"class":142},[137,347,184],{"class":153},[137,349,157],{"class":146},[137,351,352],{"class":139,"line":189},[137,353,192],{"emptyLinePlaceholder":16},[137,355,356,358,360],{"class":139,"line":195},[137,357,198],{"class":142},[137,359,202],{"class":201},[137,361,205],{"class":146},[137,363,364,366,369,371,373,376],{"class":139,"line":208},[137,365,211],{"class":142},[137,367,368],{"class":214}," businessResult",[137,370,218],{"class":142},[137,372,221],{"class":142},[137,374,375],{"class":201}," myBusinessFunction",[137,377,246],{"class":146},[137,379,380,383],{"class":139,"line":234},[137,381,382],{"class":142},"    try",[137,384,385],{"class":146}," {\n",[137,387,388,391,394],{"class":139,"line":249},[137,389,390],{"class":142},"        await",[137,392,393],{"class":201}," initConfigurationServiceX",[137,395,246],{"class":146},[137,397,398,401,404],{"class":139,"line":262},[137,399,400],{"class":146},"    } ",[137,402,403],{"class":142},"catch",[137,405,406],{"class":146}," (error) {\n",[137,408,409],{"class":139,"line":268},[137,410,411],{"class":230},"        \u002F\u002F do something... maybe\n",[137,413,414],{"class":139,"line":283},[137,415,295],{"class":146},[137,417,418,420],{"class":139,"line":292},[137,419,382],{"class":142},[137,421,385],{"class":146},[137,423,425,427,430],{"class":139,"line":424},13,[137,426,390],{"class":142},[137,428,429],{"class":201}," initConfigurationServiceY",[137,431,246],{"class":146},[137,433,435,437,439],{"class":139,"line":434},14,[137,436,400],{"class":146},[137,438,403],{"class":142},[137,440,406],{"class":146},[137,442,444],{"class":139,"line":443},15,[137,445,411],{"class":230},[137,447,449],{"class":139,"line":448},16,[137,450,295],{"class":146},[137,452,454,456],{"class":139,"line":453},17,[137,455,382],{"class":142},[137,457,385],{"class":146},[137,459,461,463,466],{"class":139,"line":460},18,[137,462,390],{"class":142},[137,464,465],{"class":201}," updateCRM1",[137,467,246],{"class":146},[137,469,471,473,475],{"class":139,"line":470},19,[137,472,400],{"class":146},[137,474,403],{"class":142},[137,476,406],{"class":146},[137,478,480],{"class":139,"line":479},20,[137,481,482],{"class":230},"        \u002F\u002F do something... maybe it depends of the CRM\n",[137,484,486],{"class":139,"line":485},21,[137,487,295],{"class":146},[137,489,491,493],{"class":139,"line":490},22,[137,492,286],{"class":142},[137,494,495],{"class":146}," businessResult;\n",[137,497,499],{"class":139,"line":498},23,[137,500,501],{"class":146},"}\n",[38,503,504],{},"C'était un compromis simple et rapide, qui nous a permis de continuer un certains temps.",[38,506,507,508,511],{},"Ici nous n'avons plus le problème des erreurs mais nous avons encore des fonctions qui sont liées de\nmanière synchrone. Typiquement si une fonction prend beaucoup de temps, elle va différer l'exécution\ndes suivantes, ainsi que l'exécution de la fonction ",[42,509,510],{},"finalizeOnboarding"," de manière générale.",[38,513,514,515,518,519,522,523,526],{},"On pourrait éventuellement répondre à ce point en les wrappant aussi d'un ",[42,516,517],{},"setImmediate",". Ce que\nnous verrons dans la suite. (aussi possible de faire sans ",[42,520,521],{},"await"," avec un simple ",[42,524,525],{},".catch",")",[38,528,529,530,526],{},"En monitoring, cette fonction nous a montré qu'elle pouvait s'exécuter en 0.2s comme parfois 4s et\nmême plus rarement 40s, en fonction du temps de réponses des API externes... Car souvent nos outils\nsont des SaaS externes ! Ce qui bien sûr peut entraîner un timeout pour le client. Ces événements\ndevenant plus fréquents, cette implémentation est de moins en moins acceptable. (problème\n:",[42,531,532],{},"performances",[38,534,535,536,45,539,526],{},"Un autre problème est la charge mentale. Un autre développeur, s’il ne connaît pas l'historique, ou\ns'il l'a oublié, peut assumer que ces fonctions ne sont pas indépendantes et que l'ordre doit être\nrespecté : Par exemple parce que chaque fonction va muter l'état en base de données et que les\nautres s'attendent à ce nouvel état (ce qui est une mauvaise pratique, mais comment savoir quand on\nne connaît pas le code par cœur ?). Sans une inspection de chaque fonction c'est difficile de\nsavoir, la confiance s'en trouve réduite, et ça freine le développeur dans sa refonte éventuelle,\ndans son ajout de feature etc... ( problèmes: ",[42,537,538],{},"perte de confiance",[42,540,541],{},"pauvre maintenabilité",[38,543,544],{},"Je continue dans ma liste des problèmes : A un autre endroit de l'application nous avions des\nfonctions à exécuter lors d'une modification sur un modèle, c’était fait via un hook :",[129,546,548],{"className":131,"code":547,"language":133,"meta":10,"style":10},"import updateCRM from \".\u002Fcrm.service.js\";\n\nasync function postHook(user) {\n  try {\n    await updateCRM(user);\n  } catch (err) {\n    logError(err);\n  }\n}\n",[42,549,550,564,568,589,596,606,616,624,629],{"__ignoreMap":10},[137,551,552,554,557,559,562],{"class":139,"line":18},[137,553,143],{"class":142},[137,555,556],{"class":146}," updateCRM ",[137,558,150],{"class":142},[137,560,561],{"class":153}," \".\u002Fcrm.service.js\"",[137,563,157],{"class":146},[137,565,566],{"class":139,"line":11},[137,567,192],{"emptyLinePlaceholder":16},[137,569,570,573,576,579,582,586],{"class":139,"line":174},[137,571,572],{"class":142},"async",[137,574,575],{"class":142}," function",[137,577,578],{"class":201}," postHook",[137,580,581],{"class":146},"(",[137,583,585],{"class":584},"s4XuR","user",[137,587,588],{"class":146},") {\n",[137,590,591,594],{"class":139,"line":189},[137,592,593],{"class":142},"  try",[137,595,385],{"class":146},[137,597,598,600,603],{"class":139,"line":195},[137,599,237],{"class":142},[137,601,602],{"class":201}," updateCRM",[137,604,605],{"class":146},"(user);\n",[137,607,608,611,613],{"class":139,"line":208},[137,609,610],{"class":146},"  } ",[137,612,403],{"class":142},[137,614,615],{"class":146}," (err) {\n",[137,617,618,621],{"class":139,"line":234},[137,619,620],{"class":201},"    logError",[137,622,623],{"class":146},"(err);\n",[137,625,626],{"class":139,"line":249},[137,627,628],{"class":146},"  }\n",[137,630,631],{"class":139,"line":262},[137,632,501],{"class":146},[38,634,635],{},"Cette fonction étant dépendante du service de CRM en question qui est un service de haut niveau et\nqui importe d'autres modules.",[38,637,638],{},"Ce qui a pour effet de coupler notre modèle de données à d'autres modèles et services d'autres\nmodules, CRM1 dépendant de tous.",[38,640,641,645,646],{},[642,643],"img",{"alt":10,"src":644},"\u002Fimages\u002FCapture-d%C3%A9cran-de-2022-08-12-15-49-11.png"," ",[647,648,649],"em",{},"Dépendances entre les modules : Intercom\nétant un CRM",[38,651,652],{},"Ce qui entraîne des problèmes d'imports, des dépendances circulaires, des difficultés à comprendre\net étendre le système.",[38,654,655,656,45,659,526],{},"(problèmes: ",[42,657,658],{},"couplages des services",[42,660,661],{},"violation d'architecture",[30,663,665],{"id":664},"solution",[34,666,667],{},"Solution",[38,669,670,671],{},"On veut pouvoir : ",[34,672,673],{},"Exécuter du code sans bloquer la requête initiale",[38,675,676],{},"Bien sûr le code métier qui doit être fait par la route et nécessaire à la réponse faite au client\nsera bloquant, mais le code indépendant qui doit être exécuté ne doit plus l'être pour la requête\ninitiale, comme la mise à jour d'un CRM par exemple.",[38,678,679,680,682],{},"Alors pour répondre au problème du non bloquant nous pouvons utiliser ",[42,681,517],{},":",[129,684,686],{"className":131,"code":685,"language":133,"meta":10,"style":10},"import serviceX from '.\u002FserviceX';\nimport serviceY from '.\u002FserviceY';\nimport CRM1 from '.\u002FCRM1';\n\nfunction finalizeOnboarding(...) {\n    const businessResult = await myBusinessFunction();\n    setImmediate(() =>\n        try {\n            await serviceX.initConfigurationServiceX();\n        } catch (error) {\n            \u002F\u002F do something... maybe it depends of the CRM\n        }\n    ));\n    setImmediate(() =>\n        try {\n            await serviceY.initConfigurationServiceY();\n        } catch (error) {\n            \u002F\u002F do something... maybe\n        }\n    ));\n    setImmediate(() =>\n        try {\n            await serviceX.updateCRM1();\n        } catch (error) {\n            \u002F\u002F do something... maybe\n        }\n    ));\n    return businessResult;\n}\n",[42,687,688,701,714,727,731,739,753,764,769,774,783,788,793,798,806,810,815,823,828,832,836,844,848,853,862,867,872,877,884],{"__ignoreMap":10},[137,689,690,692,694,696,699],{"class":139,"line":18},[137,691,143],{"class":142},[137,693,147],{"class":146},[137,695,150],{"class":142},[137,697,698],{"class":153}," '.\u002FserviceX'",[137,700,157],{"class":146},[137,702,703,705,707,709,712],{"class":139,"line":11},[137,704,143],{"class":142},[137,706,164],{"class":146},[137,708,150],{"class":142},[137,710,711],{"class":153}," '.\u002FserviceY'",[137,713,157],{"class":146},[137,715,716,718,720,722,725],{"class":139,"line":174},[137,717,143],{"class":142},[137,719,179],{"class":146},[137,721,150],{"class":142},[137,723,724],{"class":153}," '.\u002FCRM1'",[137,726,157],{"class":146},[137,728,729],{"class":139,"line":189},[137,730,192],{"emptyLinePlaceholder":16},[137,732,733,735,737],{"class":139,"line":195},[137,734,198],{"class":142},[137,736,202],{"class":201},[137,738,205],{"class":146},[137,740,741,743,745,747,749,751],{"class":139,"line":208},[137,742,211],{"class":142},[137,744,368],{"class":214},[137,746,218],{"class":142},[137,748,221],{"class":142},[137,750,375],{"class":201},[137,752,246],{"class":146},[137,754,755,758,761],{"class":139,"line":234},[137,756,757],{"class":201},"    setImmediate",[137,759,760],{"class":146},"(() ",[137,762,763],{"class":142},"=>\n",[137,765,766],{"class":139,"line":249},[137,767,768],{"class":146},"        try {\n",[137,770,771],{"class":139,"line":262},[137,772,773],{"class":146},"            await serviceX.initConfigurationServiceX();\n",[137,775,776,779,781],{"class":139,"line":268},[137,777,778],{"class":146},"        } ",[137,780,403],{"class":201},[137,782,406],{"class":146},[137,784,785],{"class":139,"line":283},[137,786,787],{"class":230},"            \u002F\u002F do something... maybe it depends of the CRM\n",[137,789,790],{"class":139,"line":292},[137,791,792],{"class":146},"        }\n",[137,794,795],{"class":139,"line":424},[137,796,797],{"class":146},"    ));\n",[137,799,800,802,804],{"class":139,"line":434},[137,801,757],{"class":201},[137,803,760],{"class":146},[137,805,763],{"class":142},[137,807,808],{"class":139,"line":443},[137,809,768],{"class":146},[137,811,812],{"class":139,"line":448},[137,813,814],{"class":146},"            await serviceY.initConfigurationServiceY();\n",[137,816,817,819,821],{"class":139,"line":453},[137,818,778],{"class":146},[137,820,403],{"class":201},[137,822,406],{"class":146},[137,824,825],{"class":139,"line":460},[137,826,827],{"class":230},"            \u002F\u002F do something... maybe\n",[137,829,830],{"class":139,"line":470},[137,831,792],{"class":146},[137,833,834],{"class":139,"line":479},[137,835,797],{"class":146},[137,837,838,840,842],{"class":139,"line":485},[137,839,757],{"class":201},[137,841,760],{"class":146},[137,843,763],{"class":142},[137,845,846],{"class":139,"line":490},[137,847,768],{"class":146},[137,849,850],{"class":139,"line":498},[137,851,852],{"class":146},"            await serviceX.updateCRM1();\n",[137,854,856,858,860],{"class":139,"line":855},24,[137,857,778],{"class":146},[137,859,403],{"class":201},[137,861,406],{"class":146},[137,863,865],{"class":139,"line":864},25,[137,866,827],{"class":230},[137,868,870],{"class":139,"line":869},26,[137,871,792],{"class":146},[137,873,875],{"class":139,"line":874},27,[137,876,797],{"class":146},[137,878,880,882],{"class":139,"line":879},28,[137,881,286],{"class":142},[137,883,495],{"class":146},[137,885,887],{"class":139,"line":886},29,[137,888,501],{"class":146},[38,890,891],{},"et",[129,893,895],{"className":131,"code":894,"language":133,"meta":10,"style":10},"import updateIntercom from 'intercom.service.js';\n\nasync function postHook(user) {\n    setImmediate(() =>\n        try {\n            await updateIntercom(user);\n        } catch(err) {\n            LogError(err)\n        }\n    ));\n}\n",[42,896,897,911,915,929,937,941,956,965,978,982,986],{"__ignoreMap":10},[137,898,899,901,904,906,909],{"class":139,"line":18},[137,900,143],{"class":142},[137,902,903],{"class":146}," updateIntercom ",[137,905,150],{"class":142},[137,907,908],{"class":153}," 'intercom.service.js'",[137,910,157],{"class":146},[137,912,913],{"class":139,"line":11},[137,914,192],{"emptyLinePlaceholder":16},[137,916,917,919,921,923,925,927],{"class":139,"line":174},[137,918,572],{"class":142},[137,920,575],{"class":142},[137,922,578],{"class":201},[137,924,581],{"class":146},[137,926,585],{"class":584},[137,928,588],{"class":146},[137,930,931,933,935],{"class":139,"line":189},[137,932,757],{"class":201},[137,934,760],{"class":146},[137,936,763],{"class":142},[137,938,939],{"class":139,"line":195},[137,940,768],{"class":146},[137,942,943,946,949,951,953],{"class":139,"line":208},[137,944,945],{"class":146},"            await ",[137,947,948],{"class":201},"updateIntercom",[137,950,581],{"class":146},[137,952,585],{"class":584},[137,954,955],{"class":146},");\n",[137,957,958,960,962],{"class":139,"line":234},[137,959,778],{"class":146},[137,961,403],{"class":201},[137,963,964],{"class":146},"(err) {\n",[137,966,967,970,972,975],{"class":139,"line":249},[137,968,969],{"class":201},"            LogError",[137,971,581],{"class":146},[137,973,974],{"class":584},"err",[137,976,977],{"class":146},")\n",[137,979,980],{"class":139,"line":262},[137,981,792],{"class":146},[137,983,984],{"class":139,"line":268},[137,985,797],{"class":146},[137,987,988],{"class":139,"line":283},[137,989,501],{"class":146},[38,991,992],{},"Cela peut résoudre le premier point (Toujours est-il que la lecture de la fonction ne s'améliore\npas).",[38,994,995],{},"Mais nous avons toujours le problème des dépendances. Pour ça, on va vouloir :",[38,997,998,999],{},"⇒ ",[34,1000,1001,1002,526],{},"Casser la dépendance et découpler notre code applicatif du code qui doit réagir.\n(",[67,1003,1006],{"href":1004,"rel":1005},"https:\u002F\u002Fmartinfowler.com\u002Fbliki\u002FInversionOfControl.html",[71],"Inversion of control",[38,1008,1009,1010,1015],{},"Par exemple, on ne veut plus que le model user dépende du service Intercom qui est un CRM. Cela\nparait évident mais je me permets d'enfoncer le clou avec le OCP\n(",[67,1011,1014],{"href":1012,"rel":1013},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FOpen%E2%80%93closed_principle",[71],"Open Closed Principle",") de SOLID\ncomme quoi le code doit être ouvert à l'extension et fermé à la modification.",[38,1017,1018],{},"Typiquement si demain on ajoute un nouveau CRM, ou un nouveau module métier je ne veux pas avoir à\nmodifier le code de User pour y insérer : \"Importer le code du nouveau module, appeler la bonne\nfonction pour l'action désirée comme mettre à jour le CRM ou initialiser le module\".",[38,1020,1021,1024],{},[34,1022,1023],{},"Mais"," on doit pouvoir ajouter un nouveau service, qui aura une fonction qu'il faut appeler, au\nbon moment, qui ne bloquera pas la fonction métier originelle, qui pourra être monitorée par\nailleurs pour ceux que ça regarde sans polluer ceux qui s'occupent du métier. Le tout étant dans un\nfichier\u002Fdossier\u002Frepo différent qui peut être la responsabilité d'une autre équipe.",[38,1026,1027],{},"Pour avoir l'inversion of control il nous faut le pattern Observer.",[38,1029,1030],{},[67,1031,1034],{"href":1032,"rel":1033},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FObserver_pattern",[71],"Observer Pattern",[38,1036,1037,1038,1043],{},"(voir ",[67,1039,1042],{"href":1040,"rel":1041},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FObserver_pattern#JavaScript",[71],"section JavaScript"," pour la suite)",[129,1045,1047],{"className":131,"code":1046,"language":133,"meta":10,"style":10},"class Subject {\n  observers = [];\n  attach(observer) {\n    this.observers.push(observer);\n  }\n  notify() {\n    this.observers.forEach((observer) => {\n      observer.update();\n    });\n  }\n}\n\nclass Observer {\n  update() {\n    \u002F\u002F Do what you have to do\n  }\n}\n",[42,1048,1049,1059,1069,1081,1095,1099,1107,1129,1139,1144,1148,1152,1156,1165,1172,1177,1181],{"__ignoreMap":10},[137,1050,1051,1054,1057],{"class":139,"line":18},[137,1052,1053],{"class":142},"class",[137,1055,1056],{"class":201}," Subject",[137,1058,385],{"class":146},[137,1060,1061,1064,1066],{"class":139,"line":11},[137,1062,1063],{"class":584},"  observers",[137,1065,218],{"class":142},[137,1067,1068],{"class":146}," [];\n",[137,1070,1071,1074,1076,1079],{"class":139,"line":174},[137,1072,1073],{"class":201},"  attach",[137,1075,581],{"class":146},[137,1077,1078],{"class":584},"observer",[137,1080,588],{"class":146},[137,1082,1083,1086,1089,1092],{"class":139,"line":189},[137,1084,1085],{"class":214},"    this",[137,1087,1088],{"class":146},".observers.",[137,1090,1091],{"class":201},"push",[137,1093,1094],{"class":146},"(observer);\n",[137,1096,1097],{"class":139,"line":195},[137,1098,628],{"class":146},[137,1100,1101,1104],{"class":139,"line":208},[137,1102,1103],{"class":201},"  notify",[137,1105,1106],{"class":146},"() {\n",[137,1108,1109,1111,1113,1116,1119,1121,1124,1127],{"class":139,"line":234},[137,1110,1085],{"class":214},[137,1112,1088],{"class":146},[137,1114,1115],{"class":201},"forEach",[137,1117,1118],{"class":146},"((",[137,1120,1078],{"class":584},[137,1122,1123],{"class":146},") ",[137,1125,1126],{"class":142},"=>",[137,1128,385],{"class":146},[137,1130,1131,1134,1137],{"class":139,"line":249},[137,1132,1133],{"class":146},"      observer.",[137,1135,1136],{"class":201},"update",[137,1138,246],{"class":146},[137,1140,1141],{"class":139,"line":262},[137,1142,1143],{"class":146},"    });\n",[137,1145,1146],{"class":139,"line":268},[137,1147,628],{"class":146},[137,1149,1150],{"class":139,"line":283},[137,1151,501],{"class":146},[137,1153,1154],{"class":139,"line":292},[137,1155,192],{"emptyLinePlaceholder":16},[137,1157,1158,1160,1163],{"class":139,"line":424},[137,1159,1053],{"class":142},[137,1161,1162],{"class":201}," Observer",[137,1164,385],{"class":146},[137,1166,1167,1170],{"class":139,"line":434},[137,1168,1169],{"class":201},"  update",[137,1171,1106],{"class":146},[137,1173,1174],{"class":139,"line":443},[137,1175,1176],{"class":230},"    \u002F\u002F Do what you have to do\n",[137,1178,1179],{"class":139,"line":448},[137,1180,628],{"class":146},[137,1182,1183],{"class":139,"line":453},[137,1184,501],{"class":146},[38,1186,1187,1188,1191,1192,1195,1196,1199],{},"Par exemple, car c'est plus clair avec des exemples, nous aurons ici ",[42,1189,1190],{},"User"," qui serait un ",[42,1193,1194],{},"subject","\net les différents modules et\u002Fou CRM des ",[42,1197,1198],{},"observers",".",[38,1201,1202,1203,1205,1206,1209,1210,1213],{},"Comme Intercom observe le user et réagit à ses changements d'état, nous avons l'",[42,1204,1078],{}," qui\nimporte le ",[42,1207,1208],{},"Subjet"," pour avoir accès à la fonction ",[42,1211,1212],{},"attach"," et s'attache.",[38,1215,1216],{},"Le rôle du sujet est juste de signaler son changement d'état via notify et la fonction update de\nl'Observer sera appelé :",[129,1218,1220],{"className":131,"code":1219,"language":133,"meta":10,"style":10},"\u002F\u002F intercom.service.js\nimport User from \".\u002Fuser\";\n\nclass intercom {\n  async update() {\n    \u002F\u002F Build data and call API\n  }\n}\n\nUser.attach(intercom);\n",[42,1221,1222,1227,1241,1245,1254,1264,1269,1273,1277,1281],{"__ignoreMap":10},[137,1223,1224],{"class":139,"line":18},[137,1225,1226],{"class":230},"\u002F\u002F intercom.service.js\n",[137,1228,1229,1231,1234,1236,1239],{"class":139,"line":11},[137,1230,143],{"class":142},[137,1232,1233],{"class":146}," User ",[137,1235,150],{"class":142},[137,1237,1238],{"class":153}," \".\u002Fuser\"",[137,1240,157],{"class":146},[137,1242,1243],{"class":139,"line":174},[137,1244,192],{"emptyLinePlaceholder":16},[137,1246,1247,1249,1252],{"class":139,"line":189},[137,1248,1053],{"class":142},[137,1250,1251],{"class":201}," intercom",[137,1253,385],{"class":146},[137,1255,1256,1259,1262],{"class":139,"line":195},[137,1257,1258],{"class":142},"  async",[137,1260,1261],{"class":201}," update",[137,1263,1106],{"class":146},[137,1265,1266],{"class":139,"line":208},[137,1267,1268],{"class":230},"    \u002F\u002F Build data and call API\n",[137,1270,1271],{"class":139,"line":234},[137,1272,628],{"class":146},[137,1274,1275],{"class":139,"line":249},[137,1276,501],{"class":146},[137,1278,1279],{"class":139,"line":262},[137,1280,192],{"emptyLinePlaceholder":16},[137,1282,1283,1286,1288],{"class":139,"line":268},[137,1284,1285],{"class":146},"User.",[137,1287,1212],{"class":201},[137,1289,1290],{"class":146},"(intercom);\n",[38,1292,1293],{},"Et dans User nous avons désormais :",[129,1295,1297],{"className":131,"code":1296,"language":133,"meta":10,"style":10},"async function postHook(user) {\n    setImmediate(() =>\n        try {\n            this.notify();\n        } catch(err) {\n            LogError(err)\n        }\n    ));\n}\n",[42,1298,1299,1313,1321,1325,1330,1338,1348,1352,1356],{"__ignoreMap":10},[137,1300,1301,1303,1305,1307,1309,1311],{"class":139,"line":18},[137,1302,572],{"class":142},[137,1304,575],{"class":142},[137,1306,578],{"class":201},[137,1308,581],{"class":146},[137,1310,585],{"class":584},[137,1312,588],{"class":146},[137,1314,1315,1317,1319],{"class":139,"line":11},[137,1316,757],{"class":201},[137,1318,760],{"class":146},[137,1320,763],{"class":142},[137,1322,1323],{"class":139,"line":174},[137,1324,768],{"class":146},[137,1326,1327],{"class":139,"line":189},[137,1328,1329],{"class":146},"            this.notify();\n",[137,1331,1332,1334,1336],{"class":139,"line":195},[137,1333,778],{"class":146},[137,1335,403],{"class":201},[137,1337,964],{"class":146},[137,1339,1340,1342,1344,1346],{"class":139,"line":208},[137,1341,969],{"class":201},[137,1343,581],{"class":146},[137,1345,974],{"class":584},[137,1347,977],{"class":146},[137,1349,1350],{"class":139,"line":234},[137,1351,792],{"class":146},[137,1353,1354],{"class":139,"line":249},[137,1355,797],{"class":146},[137,1357,1358],{"class":139,"line":262},[137,1359,501],{"class":146},[38,1361,1362],{},"L'import a disparu du fichier User ! 🎉 Nous avons donc un fichier haut niveau qui importe un\nfichier plus bas niveau (User ici) tout est rentré dans l’ordre.",[38,1364,1365],{},"Bon dans cette implémentation naïve, tout est synchrone, la gestion des erreurs n'est pas faite, on\nveut pouvoir avoir des noms d'événements différents pour un même sujet... Il reste du chemin à\nfaire.",[38,1367,1368,1369,1371,1372,1199],{},"Heureusement la plupart des langages offrent de manière native des ",[42,1370,1053],{}," pour implémenter ce\npattern et en Node nous avons ",[42,1373,1374],{},"EventEmitter",[38,1376,1377],{},[67,1378,1379],{"href":1379,"rel":1380},"https:\u002F\u002Fnodejs.org\u002Fapi\u002Fevents.html",[71],[1382,1383,1385],"h3",{"id":1384},"pourquoi-tant-de-focus-sur-ce-découplage",[34,1386,1387],{},"Pourquoi tant de focus sur ce découplage ?",[38,1389,1390],{},"En tant qu'architecte chez Indy, un de mes principaux driver aujourd'hui est de gérer une base de\ncode modifiable par une équipe de plus de 30 développeurs, et de poser les fondations pour une\néquipe de 60 développeurs.",[38,1392,1393],{},"D'expérience, les problèmes de communications priment sur les problèmes techniques pour lesquels on\ntrouve toujours une solution. C'est pourquoi découpler le code, et par extension les teams qui en\nsont responsables est un objectif très haut dans ma roadmap.",[38,1395,1396,1397,1399],{},"Typiquement, sur le code vu plus haut, la gestion du modèle ",[42,1398,585],{}," pourra être l'object d'une squad\nA, pendant que la gestion du CRM1 pourra être la responsabilité d'une squad B etc...",[38,1401,1402,1403,1408,1409,1414],{},"Le code pouvant être protégé par le système de code owners (de\n",[67,1404,1407],{"href":1405,"rel":1406},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Frepositories\u002Fmanaging-your-repositorys-settings-and-features\u002Fcustomizing-your-repository\u002Fabout-code-owners",[71],"Github","\nou ",[67,1410,1413],{"href":1411,"rel":1412},"https:\u002F\u002Fdocs.gitlab.com\u002Fee\u002Fuser\u002Fproject\u002Fcode_owners.html",[71],"GitLab","), et les squads pouvant avoir\ndes priorités différentes, je veux que chacune puisse avancer au maximum sans attendre le retour\nd'une autre.",[38,1416,1417],{},"C'est pourquoi ce découplage est si important à mes yeux.",[38,1419,1420,1423,1424,1427],{},[34,1421,1422],{},"Avec ce pattern, on peut avoir un code métier complètement découplé du code non-métier destiné à\nl’entreprise."," De plus on peut avoir autant de ",[647,1425,1426],{},"listeners"," que l’on veut, ces derniers étant\ndécouplés entre eux aussi.",[38,1429,1430],{},[647,1431,1432,1433,1438],{},"L'enjeu ici, c'est définir les interfaces entre les squads et comment je les sépare. Ici je réponds\nau fait que ce sont les Services externes qui importent User et qui réagissent dessus. Le code entre\nles squads est soumis aux\n",[67,1434,1437],{"href":1435,"rel":1436},"https:\u002F\u002Fdocs.aws.amazon.com\u002Fprescriptive-guidance\u002Flatest\u002Farchitectural-decision-records\u002Fadr-process.html",[71],"ADRs","\n(ensemble de règles et conventions que nous avons entre développeurs chez Indy) ce qui ne sera pas\nforcément le cas du code soumis à une seule squad.",[30,1440,1442],{"id":1441},"implémentation-moins-naïve",[34,1443,1444],{},"Implémentation moins naïve",[129,1446,1448],{"className":131,"code":1447,"language":133,"meta":10,"style":10},"import EventEmitter from \"events\";\nimport _ from \"lodash\";\nimport { createLogger, newEventContext } from \"..\u002F..\u002Flogger.js\";\nimport config from \"..\u002F..\u002Fconfig\";\nimport {\n  getCheckAlreadyRegisteredListener,\n  isEventListenerEnabled,\n} from \".\u002FapplicationEvents.model\";\n\nconst logger = createLogger({ namespace: \"application-events\" });\nconst eventsEnabledConfig = config.private.applicationEvents.events;\nconst listenersEnabledConfig = config.private.applicationEvents.listeners;\nconst eventListenersEnabledConfig = config.private.applicationEvents.eventListenerPairs;\n\nexport function createApplicationEvents() {\n  const eventEmitter = new EventEmitter();\n  eventEmitter.setMaxListeners(10);\n  eventEmitter.on(\"error\", (err) => {\n    logger.error({ err }, \"[ApplicationEvents] Internal event emitter error\");\n  });\n  \u002F\u002F Memoize to prevent log spam\n  const logEventWithoutListener = _.memoize(({ eventName }) => {\n    logger.info(\n      { eventName },\n      \"[ApplicationEvents] An event has been emited but no listeners are registered for this event\",\n    );\n  });\n\n  return {\n    emit(eventName, payload) {\n      \u002F\u002F Don’t trigger the event if it is disabled\n      if (!isEventListenerEnabled({ name: eventName, config: eventsEnabledConfig })) return;\n      const listenerCount = eventEmitter.listenerCount(eventName);\n      if (listenerCount === 0) {\n        logEventWithoutListener({ eventName });\n      }\n      return eventEmitter.emit(eventName, payload);\n    },\n    on(eventName, listenerName, cb) {\n      const eventListenerKey = `${eventName}-${listenerName}`;\n      \u002F\u002F Don’t register the event if the listener is disabled\n      if (!isEventListenerEnabled({ name: listenerName, config: listenersEnabledConfig })) return;\n      \u002F\u002F Don’t register the event if the pair event-listenner is disabled\n      if (!isEventListenerEnabled({ name: eventListenerKey, config: eventListenersEnabledConfig }))\n        return;\n      checkAlreadyRegisteredListener({ eventListenerKey });\n      eventEmitter.on(eventName, async (payload) => {\n        setImmediate(() =>\n          newEventContext({ eventName: eventListenerKey }, () => cb(payload, eventName)),\n        );\n      });\n    },\n  };\n}\n",[42,1449,1450,1464,1478,1492,1506,1512,1517,1522,1534,1538,1560,1572,1584,1596,1600,1612,1630,1645,1668,1684,1689,1694,1722,1732,1737,1745,1750,1754,1758,1765,1782,1788,1811,1831,1847,1856,1862,1876,1882,1904,1929,1935,1953,1959,1973,1981,1990,2013,2023,2040,2046,2052,2057,2063],{"__ignoreMap":10},[137,1451,1452,1454,1457,1459,1462],{"class":139,"line":18},[137,1453,143],{"class":142},[137,1455,1456],{"class":146}," EventEmitter ",[137,1458,150],{"class":142},[137,1460,1461],{"class":153}," \"events\"",[137,1463,157],{"class":146},[137,1465,1466,1468,1471,1473,1476],{"class":139,"line":11},[137,1467,143],{"class":142},[137,1469,1470],{"class":146}," _ ",[137,1472,150],{"class":142},[137,1474,1475],{"class":153}," \"lodash\"",[137,1477,157],{"class":146},[137,1479,1480,1482,1485,1487,1490],{"class":139,"line":174},[137,1481,143],{"class":142},[137,1483,1484],{"class":146}," { createLogger, newEventContext } ",[137,1486,150],{"class":142},[137,1488,1489],{"class":153}," \"..\u002F..\u002Flogger.js\"",[137,1491,157],{"class":146},[137,1493,1494,1496,1499,1501,1504],{"class":139,"line":189},[137,1495,143],{"class":142},[137,1497,1498],{"class":146}," config ",[137,1500,150],{"class":142},[137,1502,1503],{"class":153}," \"..\u002F..\u002Fconfig\"",[137,1505,157],{"class":146},[137,1507,1508,1510],{"class":139,"line":195},[137,1509,143],{"class":142},[137,1511,385],{"class":146},[137,1513,1514],{"class":139,"line":208},[137,1515,1516],{"class":146},"  getCheckAlreadyRegisteredListener,\n",[137,1518,1519],{"class":139,"line":234},[137,1520,1521],{"class":146},"  isEventListenerEnabled,\n",[137,1523,1524,1527,1529,1532],{"class":139,"line":249},[137,1525,1526],{"class":146},"} ",[137,1528,150],{"class":142},[137,1530,1531],{"class":153}," \".\u002FapplicationEvents.model\"",[137,1533,157],{"class":146},[137,1535,1536],{"class":139,"line":262},[137,1537,192],{"emptyLinePlaceholder":16},[137,1539,1540,1543,1546,1548,1551,1554,1557],{"class":139,"line":268},[137,1541,1542],{"class":142},"const",[137,1544,1545],{"class":214}," logger",[137,1547,218],{"class":142},[137,1549,1550],{"class":201}," createLogger",[137,1552,1553],{"class":146},"({ namespace: ",[137,1555,1556],{"class":153},"\"application-events\"",[137,1558,1559],{"class":146}," });\n",[137,1561,1562,1564,1567,1569],{"class":139,"line":283},[137,1563,1542],{"class":142},[137,1565,1566],{"class":214}," eventsEnabledConfig",[137,1568,218],{"class":142},[137,1570,1571],{"class":146}," config.private.applicationEvents.events;\n",[137,1573,1574,1576,1579,1581],{"class":139,"line":292},[137,1575,1542],{"class":142},[137,1577,1578],{"class":214}," listenersEnabledConfig",[137,1580,218],{"class":142},[137,1582,1583],{"class":146}," config.private.applicationEvents.listeners;\n",[137,1585,1586,1588,1591,1593],{"class":139,"line":424},[137,1587,1542],{"class":142},[137,1589,1590],{"class":214}," eventListenersEnabledConfig",[137,1592,218],{"class":142},[137,1594,1595],{"class":146}," config.private.applicationEvents.eventListenerPairs;\n",[137,1597,1598],{"class":139,"line":434},[137,1599,192],{"emptyLinePlaceholder":16},[137,1601,1602,1605,1607,1610],{"class":139,"line":443},[137,1603,1604],{"class":142},"export",[137,1606,575],{"class":142},[137,1608,1609],{"class":201}," createApplicationEvents",[137,1611,1106],{"class":146},[137,1613,1614,1617,1620,1622,1625,1628],{"class":139,"line":448},[137,1615,1616],{"class":142},"  const",[137,1618,1619],{"class":214}," eventEmitter",[137,1621,218],{"class":142},[137,1623,1624],{"class":142}," new",[137,1626,1627],{"class":201}," EventEmitter",[137,1629,246],{"class":146},[137,1631,1632,1635,1638,1640,1643],{"class":139,"line":453},[137,1633,1634],{"class":146},"  eventEmitter.",[137,1636,1637],{"class":201},"setMaxListeners",[137,1639,581],{"class":146},[137,1641,1642],{"class":214},"10",[137,1644,955],{"class":146},[137,1646,1647,1649,1652,1654,1657,1660,1662,1664,1666],{"class":139,"line":460},[137,1648,1634],{"class":146},[137,1650,1651],{"class":201},"on",[137,1653,581],{"class":146},[137,1655,1656],{"class":153},"\"error\"",[137,1658,1659],{"class":146},", (",[137,1661,974],{"class":584},[137,1663,1123],{"class":146},[137,1665,1126],{"class":142},[137,1667,385],{"class":146},[137,1669,1670,1673,1676,1679,1682],{"class":139,"line":470},[137,1671,1672],{"class":146},"    logger.",[137,1674,1675],{"class":201},"error",[137,1677,1678],{"class":146},"({ err }, ",[137,1680,1681],{"class":153},"\"[ApplicationEvents] Internal event emitter error\"",[137,1683,955],{"class":146},[137,1685,1686],{"class":139,"line":479},[137,1687,1688],{"class":146},"  });\n",[137,1690,1691],{"class":139,"line":485},[137,1692,1693],{"class":230},"  \u002F\u002F Memoize to prevent log spam\n",[137,1695,1696,1698,1701,1703,1706,1709,1712,1715,1718,1720],{"class":139,"line":490},[137,1697,1616],{"class":142},[137,1699,1700],{"class":214}," logEventWithoutListener",[137,1702,218],{"class":142},[137,1704,1705],{"class":146}," _.",[137,1707,1708],{"class":201},"memoize",[137,1710,1711],{"class":146},"(({ ",[137,1713,1714],{"class":584},"eventName",[137,1716,1717],{"class":146}," }) ",[137,1719,1126],{"class":142},[137,1721,385],{"class":146},[137,1723,1724,1726,1729],{"class":139,"line":498},[137,1725,1672],{"class":146},[137,1727,1728],{"class":201},"info",[137,1730,1731],{"class":146},"(\n",[137,1733,1734],{"class":139,"line":855},[137,1735,1736],{"class":146},"      { eventName },\n",[137,1738,1739,1742],{"class":139,"line":864},[137,1740,1741],{"class":153},"      \"[ApplicationEvents] An event has been emited but no listeners are registered for this event\"",[137,1743,1744],{"class":146},",\n",[137,1746,1747],{"class":139,"line":869},[137,1748,1749],{"class":146},"    );\n",[137,1751,1752],{"class":139,"line":874},[137,1753,1688],{"class":146},[137,1755,1756],{"class":139,"line":879},[137,1757,192],{"emptyLinePlaceholder":16},[137,1759,1760,1763],{"class":139,"line":886},[137,1761,1762],{"class":142},"  return",[137,1764,385],{"class":146},[137,1766,1768,1771,1773,1775,1777,1780],{"class":139,"line":1767},30,[137,1769,1770],{"class":201},"    emit",[137,1772,581],{"class":146},[137,1774,1714],{"class":584},[137,1776,45],{"class":146},[137,1778,1779],{"class":584},"payload",[137,1781,588],{"class":146},[137,1783,1785],{"class":139,"line":1784},31,[137,1786,1787],{"class":230},"      \u002F\u002F Don’t trigger the event if it is disabled\n",[137,1789,1791,1794,1797,1800,1803,1806,1809],{"class":139,"line":1790},32,[137,1792,1793],{"class":142},"      if",[137,1795,1796],{"class":146}," (",[137,1798,1799],{"class":142},"!",[137,1801,1802],{"class":201},"isEventListenerEnabled",[137,1804,1805],{"class":146},"({ name: eventName, config: eventsEnabledConfig })) ",[137,1807,1808],{"class":142},"return",[137,1810,157],{"class":146},[137,1812,1814,1817,1820,1822,1825,1828],{"class":139,"line":1813},33,[137,1815,1816],{"class":142},"      const",[137,1818,1819],{"class":214}," listenerCount",[137,1821,218],{"class":142},[137,1823,1824],{"class":146}," eventEmitter.",[137,1826,1827],{"class":201},"listenerCount",[137,1829,1830],{"class":146},"(eventName);\n",[137,1832,1834,1836,1839,1842,1845],{"class":139,"line":1833},34,[137,1835,1793],{"class":142},[137,1837,1838],{"class":146}," (listenerCount ",[137,1840,1841],{"class":142},"===",[137,1843,1844],{"class":214}," 0",[137,1846,588],{"class":146},[137,1848,1850,1853],{"class":139,"line":1849},35,[137,1851,1852],{"class":201},"        logEventWithoutListener",[137,1854,1855],{"class":146},"({ eventName });\n",[137,1857,1859],{"class":139,"line":1858},36,[137,1860,1861],{"class":146},"      }\n",[137,1863,1865,1868,1870,1873],{"class":139,"line":1864},37,[137,1866,1867],{"class":142},"      return",[137,1869,1824],{"class":146},[137,1871,1872],{"class":201},"emit",[137,1874,1875],{"class":146},"(eventName, payload);\n",[137,1877,1879],{"class":139,"line":1878},38,[137,1880,1881],{"class":146},"    },\n",[137,1883,1885,1888,1890,1892,1894,1897,1899,1902],{"class":139,"line":1884},39,[137,1886,1887],{"class":201},"    on",[137,1889,581],{"class":146},[137,1891,1714],{"class":584},[137,1893,45],{"class":146},[137,1895,1896],{"class":584},"listenerName",[137,1898,45],{"class":146},[137,1900,1901],{"class":584},"cb",[137,1903,588],{"class":146},[137,1905,1907,1909,1912,1914,1917,1919,1922,1924,1927],{"class":139,"line":1906},40,[137,1908,1816],{"class":142},[137,1910,1911],{"class":214}," eventListenerKey",[137,1913,218],{"class":142},[137,1915,1916],{"class":153}," `${",[137,1918,1714],{"class":146},[137,1920,1921],{"class":153},"}-${",[137,1923,1896],{"class":146},[137,1925,1926],{"class":153},"}`",[137,1928,157],{"class":146},[137,1930,1932],{"class":139,"line":1931},41,[137,1933,1934],{"class":230},"      \u002F\u002F Don’t register the event if the listener is disabled\n",[137,1936,1938,1940,1942,1944,1946,1949,1951],{"class":139,"line":1937},42,[137,1939,1793],{"class":142},[137,1941,1796],{"class":146},[137,1943,1799],{"class":142},[137,1945,1802],{"class":201},[137,1947,1948],{"class":146},"({ name: listenerName, config: listenersEnabledConfig })) ",[137,1950,1808],{"class":142},[137,1952,157],{"class":146},[137,1954,1956],{"class":139,"line":1955},43,[137,1957,1958],{"class":230},"      \u002F\u002F Don’t register the event if the pair event-listenner is disabled\n",[137,1960,1962,1964,1966,1968,1970],{"class":139,"line":1961},44,[137,1963,1793],{"class":142},[137,1965,1796],{"class":146},[137,1967,1799],{"class":142},[137,1969,1802],{"class":201},[137,1971,1972],{"class":146},"({ name: eventListenerKey, config: eventListenersEnabledConfig }))\n",[137,1974,1976,1979],{"class":139,"line":1975},45,[137,1977,1978],{"class":142},"        return",[137,1980,157],{"class":146},[137,1982,1984,1987],{"class":139,"line":1983},46,[137,1985,1986],{"class":201},"      checkAlreadyRegisteredListener",[137,1988,1989],{"class":146},"({ eventListenerKey });\n",[137,1991,1993,1996,1998,2001,2003,2005,2007,2009,2011],{"class":139,"line":1992},47,[137,1994,1995],{"class":146},"      eventEmitter.",[137,1997,1651],{"class":201},[137,1999,2000],{"class":146},"(eventName, ",[137,2002,572],{"class":142},[137,2004,1796],{"class":146},[137,2006,1779],{"class":584},[137,2008,1123],{"class":146},[137,2010,1126],{"class":142},[137,2012,385],{"class":146},[137,2014,2016,2019,2021],{"class":139,"line":2015},48,[137,2017,2018],{"class":201},"        setImmediate",[137,2020,760],{"class":146},[137,2022,763],{"class":142},[137,2024,2026,2029,2032,2034,2037],{"class":139,"line":2025},49,[137,2027,2028],{"class":201},"          newEventContext",[137,2030,2031],{"class":146},"({ eventName: eventListenerKey }, () ",[137,2033,1126],{"class":142},[137,2035,2036],{"class":201}," cb",[137,2038,2039],{"class":146},"(payload, eventName)),\n",[137,2041,2043],{"class":139,"line":2042},50,[137,2044,2045],{"class":146},"        );\n",[137,2047,2049],{"class":139,"line":2048},51,[137,2050,2051],{"class":146},"      });\n",[137,2053,2055],{"class":139,"line":2054},52,[137,2056,1881],{"class":146},[137,2058,2060],{"class":139,"line":2059},53,[137,2061,2062],{"class":146},"  };\n",[137,2064,2066],{"class":139,"line":2065},54,[137,2067,501],{"class":146},[30,2069,2071],{"id":2070},"quel-type-de-payload-dans-les-événements-métiers",[34,2072,2073],{},"Quel type de payload dans les événements métiers ?",[38,2075,2076],{},[34,2077,2078],{},"⇒ Payload minimal",[38,2080,2081],{},"Lorsqu'on émet un événement, on peut passer plus ou moins de data à cet évènement pour les listeners\nderrières.",[1382,2083,2085],{"id":2084},"payload-léger",[34,2086,2087],{},"Payload léger",[38,2089,2090],{},"Il s'agit de passer le minimum syndical d'informations pour que les listeners puissent fonctionner,\nce qui a pour but d'alléger au maximum la fonction métier, typiquement on passe des arguments non\nobjets, type String ou Number comme un ID :",[129,2092,2094],{"className":131,"code":2093,"language":133,"meta":10,"style":10},"async function updateUser({ userId, newFields }) {\n    await UserRepository.udpate({ userId, newFields }});\n    User.emit('updateUser', userId);\n}\n\n\u002F\u002F Other file :\nUser.on('updateUser', (userId) => {\n    const user = UserRepository.findOne({ userId });\n    await updateCRM1(user);\n})\n\n\u002F\u002F Other file :\nUser.on('updateUser', (userId) => {\n    const user = UserRepository.findOne({ userId });\n    await updateCRM2(user);\n})\n",[42,2095,2096,2119,2132,2147,2151,2155,2160,2180,2196,2204,2209,2213,2217,2237,2251,2260],{"__ignoreMap":10},[137,2097,2098,2100,2102,2105,2108,2111,2113,2116],{"class":139,"line":18},[137,2099,572],{"class":142},[137,2101,575],{"class":142},[137,2103,2104],{"class":201}," updateUser",[137,2106,2107],{"class":146},"({ ",[137,2109,2110],{"class":584},"userId",[137,2112,45],{"class":146},[137,2114,2115],{"class":584},"newFields",[137,2117,2118],{"class":146}," }) {\n",[137,2120,2121,2123,2126,2129],{"class":139,"line":11},[137,2122,237],{"class":142},[137,2124,2125],{"class":146}," UserRepository.",[137,2127,2128],{"class":201},"udpate",[137,2130,2131],{"class":146},"({ userId, newFields }});\n",[137,2133,2134,2137,2139,2141,2144],{"class":139,"line":174},[137,2135,2136],{"class":146},"    User.",[137,2138,1872],{"class":201},[137,2140,581],{"class":146},[137,2142,2143],{"class":153},"'updateUser'",[137,2145,2146],{"class":146},", userId);\n",[137,2148,2149],{"class":139,"line":189},[137,2150,501],{"class":146},[137,2152,2153],{"class":139,"line":195},[137,2154,192],{"emptyLinePlaceholder":16},[137,2156,2157],{"class":139,"line":208},[137,2158,2159],{"class":230},"\u002F\u002F Other file :\n",[137,2161,2162,2164,2166,2168,2170,2172,2174,2176,2178],{"class":139,"line":234},[137,2163,1285],{"class":146},[137,2165,1651],{"class":201},[137,2167,581],{"class":146},[137,2169,2143],{"class":153},[137,2171,1659],{"class":146},[137,2173,2110],{"class":584},[137,2175,1123],{"class":146},[137,2177,1126],{"class":142},[137,2179,385],{"class":146},[137,2181,2182,2184,2186,2188,2190,2193],{"class":139,"line":249},[137,2183,211],{"class":142},[137,2185,215],{"class":214},[137,2187,218],{"class":142},[137,2189,2125],{"class":146},[137,2191,2192],{"class":201},"findOne",[137,2194,2195],{"class":146},"({ userId });\n",[137,2197,2198,2200,2202],{"class":139,"line":262},[137,2199,237],{"class":142},[137,2201,465],{"class":201},[137,2203,605],{"class":146},[137,2205,2206],{"class":139,"line":268},[137,2207,2208],{"class":146},"})\n",[137,2210,2211],{"class":139,"line":283},[137,2212,192],{"emptyLinePlaceholder":16},[137,2214,2215],{"class":139,"line":292},[137,2216,2159],{"class":230},[137,2218,2219,2221,2223,2225,2227,2229,2231,2233,2235],{"class":139,"line":424},[137,2220,1285],{"class":146},[137,2222,1651],{"class":201},[137,2224,581],{"class":146},[137,2226,2143],{"class":153},[137,2228,1659],{"class":146},[137,2230,2110],{"class":584},[137,2232,1123],{"class":146},[137,2234,1126],{"class":142},[137,2236,385],{"class":146},[137,2238,2239,2241,2243,2245,2247,2249],{"class":139,"line":434},[137,2240,211],{"class":142},[137,2242,215],{"class":214},[137,2244,218],{"class":142},[137,2246,2125],{"class":146},[137,2248,2192],{"class":201},[137,2250,2195],{"class":146},[137,2252,2253,2255,2258],{"class":139,"line":443},[137,2254,237],{"class":142},[137,2256,2257],{"class":201}," updateCRM2",[137,2259,605],{"class":146},[137,2261,2262],{"class":139,"line":448},[137,2263,2208],{"class":146},[38,2265,2266,2267,1796,2272,2275],{},"On pourra essayer de ",[67,2268,2271],{"href":2269,"rel":2270},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FDon%27t_repeat_yourself",[71],"DRY",[647,2273,2274],{},"Don’t repeat\nyourself",") par la suite, mais sans compromettre le driver numéro 1 de découplage.",[1382,2277,2279],{"id":2278},"payload-lourd",[34,2280,2281],{},"Payload lourd",[38,2283,2284],{},"Une autre approche est de préparer le payload pour les listeners, ce qui peut éviter de recopier du\ncode, d'alléger les appels à la base de données :",[129,2286,2288],{"className":131,"code":2287,"language":133,"meta":10,"style":10},"async function updateUser({ userId, newFields }) {\n    await userRepository.udpate({ userId, newFields }});\n    const user = userRepository.findOne({ userId });\n    userEvents.emit('updateUser', user);\n}\n\n\u002F\u002F Other file :\nuserEvents.on('updateUser', (user) => {\n    await updateCRM1(user);\n})\n\n\u002F\u002F Other file :\nuserEvents.on('updateUser', (user) => {\n    await updateCRM2(user);\n})\n",[42,2289,2290,2308,2319,2333,2347,2351,2355,2359,2380,2388,2392,2396,2400,2420,2428],{"__ignoreMap":10},[137,2291,2292,2294,2296,2298,2300,2302,2304,2306],{"class":139,"line":18},[137,2293,572],{"class":142},[137,2295,575],{"class":142},[137,2297,2104],{"class":201},[137,2299,2107],{"class":146},[137,2301,2110],{"class":584},[137,2303,45],{"class":146},[137,2305,2115],{"class":584},[137,2307,2118],{"class":146},[137,2309,2310,2312,2315,2317],{"class":139,"line":11},[137,2311,237],{"class":142},[137,2313,2314],{"class":146}," userRepository.",[137,2316,2128],{"class":201},[137,2318,2131],{"class":146},[137,2320,2321,2323,2325,2327,2329,2331],{"class":139,"line":174},[137,2322,211],{"class":142},[137,2324,215],{"class":214},[137,2326,218],{"class":142},[137,2328,2314],{"class":146},[137,2330,2192],{"class":201},[137,2332,2195],{"class":146},[137,2334,2335,2338,2340,2342,2344],{"class":139,"line":189},[137,2336,2337],{"class":146},"    userEvents.",[137,2339,1872],{"class":201},[137,2341,581],{"class":146},[137,2343,2143],{"class":153},[137,2345,2346],{"class":146},", user);\n",[137,2348,2349],{"class":139,"line":195},[137,2350,501],{"class":146},[137,2352,2353],{"class":139,"line":208},[137,2354,192],{"emptyLinePlaceholder":16},[137,2356,2357],{"class":139,"line":234},[137,2358,2159],{"class":230},[137,2360,2361,2364,2366,2368,2370,2372,2374,2376,2378],{"class":139,"line":249},[137,2362,2363],{"class":146},"userEvents.",[137,2365,1651],{"class":201},[137,2367,581],{"class":146},[137,2369,2143],{"class":153},[137,2371,1659],{"class":146},[137,2373,585],{"class":584},[137,2375,1123],{"class":146},[137,2377,1126],{"class":142},[137,2379,385],{"class":146},[137,2381,2382,2384,2386],{"class":139,"line":262},[137,2383,237],{"class":142},[137,2385,465],{"class":201},[137,2387,605],{"class":146},[137,2389,2390],{"class":139,"line":268},[137,2391,2208],{"class":146},[137,2393,2394],{"class":139,"line":283},[137,2395,192],{"emptyLinePlaceholder":16},[137,2397,2398],{"class":139,"line":292},[137,2399,2159],{"class":230},[137,2401,2402,2404,2406,2408,2410,2412,2414,2416,2418],{"class":139,"line":424},[137,2403,2363],{"class":146},[137,2405,1651],{"class":201},[137,2407,581],{"class":146},[137,2409,2143],{"class":153},[137,2411,1659],{"class":146},[137,2413,585],{"class":584},[137,2415,1123],{"class":146},[137,2417,1126],{"class":142},[137,2419,385],{"class":146},[137,2421,2422,2424,2426],{"class":139,"line":434},[137,2423,237],{"class":142},[137,2425,2257],{"class":201},[137,2427,605],{"class":146},[137,2429,2430],{"class":139,"line":443},[137,2431,2208],{"class":146},[38,2433,2434],{},"Ici on introduit donc une fonction non nécessaire au métier dans la première fonction updateUser, le\ncontrat entre l'évènement et ces listeners est plus fort. Donc la première team doit avoir une\nmeilleure connaissance des listeners potentiellement gérés par d'autres personnes.",[38,2436,2437],{},"Le compromis ici est une interface et une contrainte plus forte entre les teams composées d'humains\npour une optimisation du code.",[30,2439,2441],{"id":2440},"conclusion",[34,2442,2443],{},"Conclusion",[38,2445,2446],{},"Il se trouve que la plupart du temps l'optimisation de code de la solution 2 est négligeable comparé\nau coût de communication humain, et c'est pourquoi ce n'est pas notre priorité et que cette solution\nn'est retenue que dans les cas prouvés comme étant problématiques.",[38,2448,2449],{},"Les deux peuvent fonctionner, mais étant donné que notre driver architectural est le découplage.\nNous allons retenir la solution 1 du payload léger, quitte à gérer les côtés négatifs dans un second\ntemps.",[38,2451,2452],{},[34,2453,2454],{},"On utilise un pattern Observer basé sur le EventEmitter de Node pour découpler le code et les\nteams aux maximum. En ce qui concerne les évènements, le payload sera minimal et les listeners\nseront aussi découpés dans des fichiers différents.",[2456,2457,2458],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":10,"searchDepth":11,"depth":11,"links":2460},[2461,2462,2463,2464,2467,2468,2472],{"id":32,"depth":11,"text":36},{"id":61,"depth":11,"text":62},{"id":116,"depth":11,"text":119},{"id":664,"depth":11,"text":667,"children":2465},[2466],{"id":1384,"depth":174,"text":1387},{"id":1441,"depth":11,"text":1444},{"id":2070,"depth":11,"text":2073,"children":2469},[2470,2471],{"id":2084,"depth":174,"text":2087},{"id":2278,"depth":174,"text":2281},{"id":2440,"depth":11,"text":2443},"2022-09-01","Qu’entendons-nous par événement métier chez Indy ? ","fr",{},"\u002Farticles\u002F2022-09-01-application-events",{"title":25,"description":2474},"articles\u002F2022-09-01-application-events",[2481],"Tech","EKCFqe9k1y4Wxx6V7sbRgbX2TZBoToNO_Dwgl_AR1Wg",{"id":2484,"title":2485,"author":26,"body":2486,"date":2821,"description":2822,"extension":13,"lang":2475,"meta":2823,"navigation":16,"path":2824,"published":16,"readingTime":283,"seo":2825,"stem":2826,"tags":2827,"__hash__":2830},"articles\u002Farticles\u002F2022-03-10-introduction-du-bus-dentreprise-chez-indy.md","Introduction du bus d'entreprise chez Indy",{"type":7,"value":2487,"toc":2811},[2488,2491,2494,2497,2500,2503,2511,2514,2517,2525,2530,2533,2536,2539,2542,2545,2548,2551,2554,2559,2562,2565,2570,2578,2583,2586,2594,2597,2611,2614,2617,2625,2628,2631,2648,2651,2659,2662,2665,2668,2673,2678,2681,2690,2693,2696,2704,2707,2718,2726,2729,2737,2740,2745,2748,2755,2758,2761,2769,2772,2775,2780,2782,2785,2788,2790,2796,2802,2805,2808],[38,2489,2490],{},"Récemment, chez Indy nous avons pris la décision d'ajouter un bus d'entreprise à la stack technique.",[38,2492,2493],{},"Nous allons passer en revue certaines des raisons qui nous ont poussé à faire ce choix.",[38,2495,2496],{},"Contexte : L'équipe tech et product est désormais composée de 39 personnes dont 32 développeurs.",[38,2498,2499],{},"Cette décision n'est pas forcément facile à prendre car elle introduit de la complexité nouvelle\ndans la stack technique. Ainsi que des techniques et des pratiques pour lesquelles la plupart des\ndéveloppeurs de l'équipe ne sont pas encore familiers.",[38,2501,2502],{},"Néanmoins après avoir réfléchi à la question et à la connaissance des personnalités qui forment\nl'équipe, je pense que c'est la voie de la scalabilité pour la boîte. Ce ne sera peut-être pas votre\ncas, donc je partage notre prise de décision et je ferai une suite d'articles sur l'adoption de\ncette pratique après quelques mois puis après un an.",[30,2504,2506,2510],{"id":2505},"fils-conducteurs",[67,2507],{"href":2508,"rel":2509},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#fils-conducteurs",[71],"Fils conducteurs",[38,2512,2513],{},"Pour expliquer comment l'architecture évolue, on peut citer nos fils conducteurs, et résumer la\nsituation actuelle.",[38,2515,2516],{},"Nous avons un monolithe principal (Indy) sur lequel se trouvent les règles métiers, qui sert de\nbackend à notre frontend et qui synchronise aussi un petit régiment d'outils externes. Ces outils ne\nsont pas liés à notre produit principal mais participent au bon fonctionnement de l'entreprise,\nnotamment la synchronisation de nos différents CRM.",[1382,2518,2520,2524],{"id":2519},"historique",[67,2521],{"href":2522,"rel":2523},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#historique",[71],"Historique",[38,2526,2527],{},[642,2528],{"alt":10,"src":2529},"\u002Fimages\u002Fhistorique.gif",[38,2531,2532],{},"L'app a commencé simplement avec une connection Indy \u003C-> Intercom. Intercom est l'outil utilisé par\nnos care pour faire la relation client. La première intégration demandée a été de remonter des\ninformations du client sur Intercom comme : L'utilisateur est-il abonné ? A-t-il terminé sa clôture,\ndans quel régime fiscal se trouve-t-il ?",[38,2534,2535],{},"Puis nous avons eu une intégration Gsheet avec l'équipe marketing qui sortait des données d'Intercom\nà des fins d'analyses. Synchro au début manuelle qui est passé automatique.",[38,2537,2538],{},"Un peu plus tard, nous avons intégré le CRM de l'équipe Sales : pipedrive, ici aussi nous avons du\nremonter des informations du produit vers Pipedrive pour simplifier le travail des Sales en\nautomatisant certaines actions sur pipedrive. Mais ici la relation est à double sens car nous\ndevions récupérer des informations sur le commercial pour l'équipe marketing, ça remontait alors\ndans l'application pour aller dans Intercom pour aller ensuite dans GSheet (!).",[38,2540,2541],{},"\u002F!\\ Premier warning d'architecture ici : L'app Indy servait d'intermédiaire pour synchroniser\npipedrive et notre BI de l'époque, code qui ne servait pas du tout l'intention originelle de l'app\nqui était de servir le client, c'était du code d'entreprise pur.",[38,2543,2544],{},"Puis nous avons ajouté Metabase qui aggrège les données des différents CRM ainsi que les données de\nl'application dans un outil qui sert à faire de la visualisation de données et de la BI.",[38,2546,2547],{},"Avec la croissance, on a continué à empiler les intégrations : Ringover pour des besoins sales, puis\nTrello pour de l'automatisation produit.",[38,2549,2550],{},"Nous avons continué avec des app internes qui manipulent l'API de CRM pour des besoins internes\n(autour de l'affectation d'Intercom), des intégrations entre nos CRM : Intercom et Pipedrive car des\ndonnées de l'un étaient nécessaires dans l'autre.",[38,2552,2553],{},"Et le dernier ajout Slack qui s'intègre avec quasiment tous les autres services pour avoir nos infos\ndans notre outil de communication.",[38,2555,2556],{},[642,2557],{"alt":10,"src":2558},"\u002Fimages\u002Fwell_that_escalated_quickly.jpeg",[38,2560,2561],{},"Le schéma devient vite complexe et il devient difficile pour un développeur, même ancien, de\ncomprendre le flux de données et d'expliquer tout ce qu'il se passe sur notre système d'information.",[38,2563,2564],{},"Et ce schéma ne montre pas toute l'intégration de monitoring de l'application et des intégrations !\nCe n'était pas très gênant au début, mais cette complexité a conduit à des régressions.",[38,2566,2567],{},[34,2568,2569],{},"Un fil conducteur sera alors : Le besoin d'être en mesure d'expliquer le sens des flux de\ndonnées.",[78,2571,2572,2575],{},[81,2573,2574],{},"Avoir une architecture plus explicite et systématique au niveau des synchros",[81,2576,2577],{},"Que le sens de circulation des données soit plus explicite et systématique aussi",[38,2579,2580],{},[34,2581,2582],{},"Un autre fil conducteur sera alors : Être en capacité de facilement rajouter une synchro sur le SI\nexistant sans modifier le produit comptable et les autres services",[38,2584,2585],{},"Typiquement si demain on ajoute un nouveau CRM, il doit pouvoir écouter les évènements existants,\nfaire sa propre synchro et en cas de crash, le faire sans impacter les autres services. En\nparticulier le produit où se trouvent les clients.",[1382,2587,2589,2593],{"id":2588},"exemple-déchec-suite-à-cet-état",[67,2590],{"href":2591,"rel":2592},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#exemple-d%C3%A9chec-suite-%C3%A0-cet-%C3%A9tat-",[71],"Exemple d'échec suite à cet état :",[38,2595,2596],{},"Certaines API de nos CRM nous imposent un rate limit, ce qui n'est pas problématique au début mais\navec la croissance de la boite on vient toucher ce rate limit inévitablement et une fois atteint, la\nplupart de nos requêtes échouent et notre synchronisation est perdue. C'est arrivé et le début du\ndébug fut long et fastidieux car nous avions plein d'intégrations avec ce CRM, chacune plus ou moins\nbien monitorée. Il était alors difficile de trouver l'intégration qui posait problème et qui\nexplosait le rate limit :",[78,2598,2599,2602,2605,2608],{},[81,2600,2601],{},"Était-ce notre application principale ?",[81,2603,2604],{},"Était-ce les ZAP (de Zapier) mis en place ?",[81,2606,2607],{},"Était-ce son intégration avec un autre CRM ?",[81,2609,2610],{},"Était-ce les jobs journaliers ? etc...",[38,2612,2613],{},"Une fois trouvé, comment respecter ce rate limit ? Comment le répartir sur les différentes\nintégrations ? Alors qu'elles n'ont pas la même criticité, qu'elles ne sont pas actives au même\nmoment dans la journée, est-ce qu'il faut mettre en place une pile etc...",[38,2615,2616],{},"Nous avons perdu quelques semaines à résoudre ce point-là.",[1382,2618,2620,2624],{"id":2619},"volonté-de-changer",[67,2621],{"href":2622,"rel":2623},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#volont%C3%A9-de-changer",[71],"Volonté de changer",[38,2626,2627],{},"Ces problématiques sont assez classiques dans l'industrie et il existe beaucoup de littérature sur\nle sujet. Après nous être renseigné, nous avons décidé d'agir et de nous aligner sur ce qui se fait\nde manière générale sans chercher à être trop original.",[38,2629,2630],{},"Avec la croissance de la base de code et de l'équipe, nos drivers architecturaux (et humains) sont\ndorénavant :",[78,2632,2633,2638,2643],{},[81,2634,2635],{},[34,2636,2637],{},"Découplage et autonomie des squads",[81,2639,2640],{},[34,2641,2642],{},"Pas d'erreur sur le produit principal",[81,2644,2645],{},[34,2646,2647],{},"Garder la volonté d'extension rapide de notre produit et SI",[38,2649,2650],{},"=> Il faut découpler ce qui peut l'être quitte à échanger un peu de complexité et introduire de\nnouvelles technologies. Les synchros propres à une squad et qui ne font pas partie du produit\ncomptable sont des bons candidats.",[1382,2652,2654,2658],{"id":2653},"alléger-notre-codebase-principale",[67,2655],{"href":2656,"rel":2657},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#all%C3%A9ger-notre-codebase-principale",[71],"Alléger notre codebase principale",[38,2660,2661],{},"Pourquoi sortir du code ? Retirer du code sans impacter nos fonctionnalités a toujours l'avantage de\nle rendre plus simple avec une interface exposée aux bugs plus faible. Il en va de même pour les\ndépendances, car la plupart des synchros viennent avec leur SDK et dépendances. L'envie de sortir le\ncode de ces synchros, qui pèse dans la codebase du produit principal, s'est fait ressentir suite à\ndes expériences parfois douloureuses sur les problématiques citées plus haut. Pour alléger le code\ndu dépôt principal et du monolithe, ainsi qu'alléger ses dépendances, nous avons décidé de sortir le\ncode qui n'était pas vital à notre métier : la compta.",[38,2663,2664],{},"Ceci permet d'avoir des équipes qui travaillent sur des dépôts différents pour du code qui a un but\net une criticité complètement différentes, les laissant faire leur choix d'architecture, de CI et de\nprocess.",[38,2666,2667],{},"Je voulais aussi un découplage maximal, c'est pourquoi la solution d'appeler des services\ndirectement en HTTP ne me plaisait pas. Le pattern pub\u002Fsub est tout indiqué ici et est largement\ndiscuté dans la littérature, ce qui nous a amené assez rapidement sur cette proposition :",[38,2669,2670],{},[642,2671],{"alt":10,"src":2672},"\u002Fimages\u002Farchitecture-bus-et-events.png",[38,2674,2675],{},[647,2676,2677],{},"Tous les services ne sont pas représentés...",[38,2679,2680],{},"Le BUS fait office de SPOF (single point of failure), mais si le BUS tombe il n'y a pas d'impact sur\nle service pour les clients finaux.",[2682,2683,2685,2689],"h4",{"id":2684},"impacts-sur-le-respect-de-solid",[67,2686],{"href":2687,"rel":2688},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#impacts-sur-le-respect-de-solid",[71],"Impacts sur le respect de SOLID",[38,2691,2692],{},"Le principe SRP (Single Responsibility Principle) est mieux respecté, il est plus facile de faire\névoluer chaque petit module. Si l'API d'intercom change, on change le service en question et pas\nl'application de comptabilité, de même pour les autres services cloud.",[38,2694,2695],{},"Le principe OCP (Open\u002FClosed Principle) est mieux respecté aussi. Si demain on veut ajouter un\nservice, on peut le rajouter comme consumer du BUS sans toucher au reste du SI.",[1382,2697,2699,2703],{"id":2698},"gestion-de-la-donnée-nécessaire-aux-services-externes",[67,2700],{"href":2701,"rel":2702},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#gestion-de-la-donn%C3%A9e-n%C3%A9cessaire-aux-services-externes",[71],"Gestion de la donnée nécessaire aux services externes",[38,2705,2706],{},"Une question se pose alors : comment envoyer sur le bus la donnée métier nécessaire aux applications\nde synchronisation de nos CRM ? Typiquement, lors d'un event user\u002Fsubscribe par exemple, le nom de\nl'event et l'ID de l'utilisateur en soit ne suffisent pas. Le service pipedrive ou Intercom qui\nécoute sur le bus va vouloir que de la donnée soit attachée à l'évènement. Il existe alors trois\nsolutions :",[78,2708,2709,2712,2715],{},[81,2710,2711],{},"Base de données partagée",[81,2713,2714],{},"en PULL : les services appellent le produit principal à travers le réseaux",[81,2716,2717],{},"en PUSH : le produit envoie la donnée sur le bus",[2682,2719,2721,2725],{"id":2720},"_1-shared-database",[67,2722],{"href":2723,"rel":2724},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#1-shared-database-",[71],"1. Shared database :",[38,2727,2728],{},"Très rapidement écarté car cela ne sert à rien de découpler le code s'il reste couplé par le schéma\nde base de données. Ce modèle est trop fragile et bug-prone.",[2682,2730,2732,2736],{"id":2731},"_2-pull-le-service-va-chercher-linformation-par-api",[67,2733],{"href":2734,"rel":2735},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#2--pull-le-service-va-chercher-linformation-par-api-",[71],"2. PULL Le service va chercher l'information par API :",[38,2738,2739],{},"Dans ce modèle, on essaye de garder le produit principal le plus simple possible en envoyant un\névènement technique sans données métiers (uniquement des IDs). C'est au CRM en question de savoir\nquelles données il désire et d'aller les récupérer en interrogeant, par exemple, une simple API CRUD\nqui expose lesdites données.",[38,2741,2742],{},[642,2743],{"alt":10,"src":2744},"\u002Fimages\u002Fbus-data-v2.png",[38,2746,2747],{},"Avantages :",[38,2749,2750,2751,2754],{},"Le code de l'émetteur de l'event reste simple. On ne fait pas apparaître une\nfonction ",[42,2752,2753],{},"buildEvent"," qui va aggréger des données qui, de première abord, n'ont rien à voir avec le\ncas d'utilisation car c'est une spécifité de l'app de sync d'un CRM.",[38,2756,2757],{},"Désavantages :",[38,2759,2760],{},"Nous avons des routes qui sont potentiellement utilisées par des clients (l'interface) ou des apps\ninternes. Il faut pouvoir les monitorer pour savoir si elles sont toujours utilisées ou non.\nBeaucoup de données transitent par le réseau donc il y a plus de risques d'erreur.",[2682,2762,2764,2768],{"id":2763},"_3-push-la-data-est-formatté-par-georges-puis-accroché-à-lévènement",[67,2765],{"href":2766,"rel":2767},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#3--push-la-data-est-formatt%C3%A9-par-georges-puis-accroch%C3%A9-%C3%A0-l%C3%A9v%C3%A8nement",[71],"3. PUSH La data est formatté par Georges puis accroché à l'évènement",[38,2770,2771],{},"Ici la donnée est formattée dans l'émetteur directement puis attachée à l'event pour être envoyée\nsur le bus. Les modules n'ont pas à aller interroger l'émetteur pour récupérer des données\nsupplémentaires.",[38,2773,2774],{},"L'enjeu ici est de ne pas polluer le cas d'utilisation d'une route métier avec l'agrégation de\ndonnée qui serait un enjeu non fonctionnel, juste une spécificité de nos outils de sync.",[38,2776,2777],{},[642,2778],{"alt":10,"src":2779},"\u002Fimages\u002Fbus-data-v3.png",[38,2781,2747],{},[38,2783,2784],{},"Possible de retrouver un cas d'utilisation simple débarassé des fonctions buildEvent (qui ne seront\npas dans le même fichier).",[38,2786,2787],{},"On évite les appels de nos app vers Georges et les appels API qui partent dans tous les sens et qui\nsont dur à monitorer.",[38,2789,2757],{},[38,2791,2792,2793,2795],{},"Un peu plus de discipline dans le code de l'émetteur . On veut éviter que la fonction\nde ",[42,2794,2753],{},", celle qui va aggréger des données métiers nécessaires à nos outils mais non\nnécessaires au cas d'utilisation soit dans la route de ce dernier. Ceci peut être accompli avec des\névènements internes et un pattern Observer (voir futur billet de blog).",[30,2797,2798,2443],{"id":2440},[67,2799],{"href":2800,"rel":2801},"https:\u002F\u002Fgithub.com\u002FGeorgesTech\u002Fblog-content\u002Fblob\u002Fmain\u002FBlog\u002FIntroduction%20du%20bus%20d'entreprise.md#conclusion",[71],[38,2803,2804],{},"Je peux avoir des squads spécialisées sur le produit comptable qui n'ont pas connaissance des outils\ninternes de la boîte !",[38,2806,2807],{},"Je peux avoir des squads spécialisées sur la maintenance de nos outils internes sans connaissance\nsur le produit comptable au-delà des données qui les intéressent (celle qui transitent sur le BUS).",[38,2809,2810],{},"L'enjeu, ici, est désormais de définir et respecter un contrat de données qui va passer sur le BUS.\nCela peut être le rôle du CTO (qui est inter-squads), d'une personne de l'équipe data, d'un OPS. Le\nrôle étant de faire respecter ce contrat et vérifier la cohérence des données qui passent sur le\nBUS, (c'est-à-dire donner la spécification des buildEvents).",{"title":10,"searchDepth":11,"depth":11,"links":2812},[2813,2820],{"id":2505,"depth":11,"text":2510,"children":2814},[2815,2816,2817,2818,2819],{"id":2519,"depth":174,"text":2524},{"id":2588,"depth":174,"text":2593},{"id":2619,"depth":174,"text":2624},{"id":2653,"depth":174,"text":2658},{"id":2698,"depth":174,"text":2703},{"id":2440,"depth":11,"text":2443},"2022-03-10","Récemment, chez Indy nous avons pris la décision d’ajouter un bus d’entreprise à la stack technique.",{},"\u002Farticles\u002F2022-03-10-introduction-du-bus-dentreprise-chez-indy",{"title":2485,"description":2822},"articles\u002F2022-03-10-introduction-du-bus-dentreprise-chez-indy",[2481,2828,2829],"Scalabilité","Architecture","i0Y5VorTD88p3brFa-dZ5TRZXv7U71xaNeeX7xlwDjE",[2832,2845,2857,2870,2883,2895,2907,2920,2933,2946,2958,2970,2983,2995,3007,3019,3031,3043,3055,3067,3074,3086,3098,3111,3123,3135],{"id":2833,"title":2834,"body":2835,"description":10,"extension":13,"meta":2839,"name":2840,"navigation":16,"path":2841,"readingTime":18,"seo":2842,"stem":2843,"__hash__":2844},"authors\u002Fauthors\u002Falexandre-guillon.md","Software Engineer",{"type":7,"value":2836,"toc":2837},[],{"title":10,"searchDepth":11,"depth":11,"links":2838},[],{},"Alexandre Guillon","\u002Fauthors\u002Falexandre-guillon",{"title":2834,"description":10},"authors\u002Falexandre-guillon","4tf48mjyjFNqItOHaulICbrjeCyMag1o6801uHeTz98",{"id":2846,"title":2834,"body":2847,"description":10,"extension":13,"meta":2851,"name":2852,"navigation":16,"path":2853,"readingTime":18,"seo":2854,"stem":2855,"__hash__":2856},"authors\u002Fauthors\u002Falexis-ablain.md",{"type":7,"value":2848,"toc":2849},[],{"title":10,"searchDepth":11,"depth":11,"links":2850},[],{},"Alexis Ablain","\u002Fauthors\u002Falexis-ablain",{"title":2834,"description":10},"authors\u002Falexis-ablain","_SIAtB7f-39e5t3GiJof81NP47s6MGo2n4gaHkTy1uQ",{"id":2858,"title":2859,"body":2860,"description":10,"extension":13,"meta":2864,"name":2865,"navigation":16,"path":2866,"readingTime":18,"seo":2867,"stem":2868,"__hash__":2869},"authors\u002Fauthors\u002Faxel-shaita.md","Engineering Manager",{"type":7,"value":2861,"toc":2862},[],{"title":10,"searchDepth":11,"depth":11,"links":2863},[],{},"Axel Shaïta","\u002Fauthors\u002Faxel-shaita",{"title":2859,"description":10},"authors\u002Faxel-shaita","fK0argUhsBkWLjpTAhY13oYLVzQthcEYkCEdtHWmIgE",{"id":2871,"title":2872,"body":2873,"description":10,"extension":13,"meta":2877,"name":2878,"navigation":16,"path":2879,"readingTime":18,"seo":2880,"stem":2881,"__hash__":2882},"authors\u002Fauthors\u002Fbaptiste-faure.md","Head of Talent Acquisition",{"type":7,"value":2874,"toc":2875},[],{"title":10,"searchDepth":11,"depth":11,"links":2876},[],{},"Baptiste Faure","\u002Fauthors\u002Fbaptiste-faure",{"title":2872,"description":10},"authors\u002Fbaptiste-faure","ELisToYtcgHmgdVWZkCclTPV6exZtfyXqhpx1jjbJHs",{"id":2884,"title":2834,"body":2885,"description":10,"extension":13,"meta":2889,"name":2890,"navigation":16,"path":2891,"readingTime":18,"seo":2892,"stem":2893,"__hash__":2894},"authors\u002Fauthors\u002Fbenjamin-bouillot.md",{"type":7,"value":2886,"toc":2887},[],{"title":10,"searchDepth":11,"depth":11,"links":2888},[],{},"Benjamin Bouillot","\u002Fauthors\u002Fbenjamin-bouillot",{"title":2834,"description":10},"authors\u002Fbenjamin-bouillot","tbhCFZyfTt7ZM5b5YgqQ2nhgnSTl8BweaQQryc87fHo",{"id":2896,"title":2859,"body":2897,"description":10,"extension":13,"meta":2901,"name":2902,"navigation":16,"path":2903,"readingTime":18,"seo":2904,"stem":2905,"__hash__":2906},"authors\u002Fauthors\u002Fcedric-nicoloso.md",{"type":7,"value":2898,"toc":2899},[],{"title":10,"searchDepth":11,"depth":11,"links":2900},[],{},"Cédric Nicoloso","\u002Fauthors\u002Fcedric-nicoloso",{"title":2859,"description":10},"authors\u002Fcedric-nicoloso","ibSoh4VZYiWYTuLOnZTedaAfcnvet1Q9H7ogW0LgorY",{"id":2908,"title":2909,"body":2910,"description":10,"extension":13,"meta":2914,"name":2915,"navigation":16,"path":2916,"readingTime":18,"seo":2917,"stem":2918,"__hash__":2919},"authors\u002Fauthors\u002Fdavid-touzet.md","Staff Engineer",{"type":7,"value":2911,"toc":2912},[],{"title":10,"searchDepth":11,"depth":11,"links":2913},[],{},"David Touzet","\u002Fauthors\u002Fdavid-touzet",{"title":2909,"description":10},"authors\u002Fdavid-touzet","dHWwnQxb1Ubt-WwXWEODGEo9AFoq1cJUhfg3kdnYSBM",{"id":2921,"title":2922,"body":2923,"description":10,"extension":13,"meta":2927,"name":2928,"navigation":16,"path":2929,"readingTime":18,"seo":2930,"stem":2931,"__hash__":2932},"authors\u002Fauthors\u002Feloise-chizat.md","Data Engineer",{"type":7,"value":2924,"toc":2925},[],{"title":10,"searchDepth":11,"depth":11,"links":2926},[],{},"Eloïse Chizat","\u002Fauthors\u002Feloise-chizat",{"title":2922,"description":10},"authors\u002Feloise-chizat","Utd72Vm9qT4hh2ZbFi6a2_nXw5Wb494Ed_HL1ra5yw8",{"id":2934,"title":2935,"body":2936,"description":10,"extension":13,"meta":2940,"name":2941,"navigation":16,"path":2942,"readingTime":18,"seo":2943,"stem":2944,"__hash__":2945},"authors\u002Fauthors\u002Femmanuel-auclair.md","Staff engineer",{"type":7,"value":2937,"toc":2938},[],{"title":10,"searchDepth":11,"depth":11,"links":2939},[],{},"Emmanuel Auclair","\u002Fauthors\u002Femmanuel-auclair",{"title":2935,"description":10},"authors\u002Femmanuel-auclair","MtsA8THNLEn0dTtYEIQaGwDuf7MjQL55IOeei5gugEg",{"id":2947,"title":2834,"body":2948,"description":10,"extension":13,"meta":2952,"name":2953,"navigation":16,"path":2954,"readingTime":18,"seo":2955,"stem":2956,"__hash__":2957},"authors\u002Fauthors\u002Fhoreb-parraud.md",{"type":7,"value":2949,"toc":2950},[],{"title":10,"searchDepth":11,"depth":11,"links":2951},[],{},"Horeb Parraud","\u002Fauthors\u002Fhoreb-parraud",{"title":2834,"description":10},"authors\u002Fhoreb-parraud","ajjsnUX4ohZI-ghMdbb92q_taWDkKXVZSLZXoAeLQtg",{"id":2959,"title":2859,"body":2960,"description":10,"extension":13,"meta":2964,"name":2965,"navigation":16,"path":2966,"readingTime":18,"seo":2967,"stem":2968,"__hash__":2969},"authors\u002Fauthors\u002Fhugo-contreras.md",{"type":7,"value":2961,"toc":2962},[],{"title":10,"searchDepth":11,"depth":11,"links":2963},[],{},"Hugo Contreras","\u002Fauthors\u002Fhugo-contreras",{"title":2859,"description":10},"authors\u002Fhugo-contreras","2nc3VMu9ASq9Z6Pwx2-7-Ye991Pww4p-UEDBQFfjF-Q",{"id":2971,"title":2972,"body":2973,"description":10,"extension":13,"meta":2977,"name":2978,"navigation":16,"path":2979,"readingTime":18,"seo":2980,"stem":2981,"__hash__":2982},"authors\u002Fauthors\u002Fjulien-tassin.md","Head of Engineering",{"type":7,"value":2974,"toc":2975},[],{"title":10,"searchDepth":11,"depth":11,"links":2976},[],{},"Julien Tassin","\u002Fauthors\u002Fjulien-tassin",{"title":2972,"description":10},"authors\u002Fjulien-tassin","iUIHI7SITje38Jh9X9uvYs4-VsHx4eCdt6hAlyLFG_o",{"id":2984,"title":2834,"body":2985,"description":10,"extension":13,"meta":2989,"name":2990,"navigation":16,"path":2991,"readingTime":18,"seo":2992,"stem":2993,"__hash__":2994},"authors\u002Fauthors\u002Flaurent-renard.md",{"type":7,"value":2986,"toc":2987},[],{"title":10,"searchDepth":11,"depth":11,"links":2988},[],{},"Laurent Renard","\u002Fauthors\u002Flaurent-renard",{"title":2834,"description":10},"authors\u002Flaurent-renard","5BP7Ed-pt1SQHjh0UJ1XUrlLTcdlFaDoKBCP4deHq8A",{"id":2996,"title":2834,"body":2997,"description":10,"extension":13,"meta":3001,"name":3002,"navigation":16,"path":3003,"readingTime":18,"seo":3004,"stem":3005,"__hash__":3006},"authors\u002Fauthors\u002Fleo-martin.md",{"type":7,"value":2998,"toc":2999},[],{"title":10,"searchDepth":11,"depth":11,"links":3000},[],{},"Léo Martin","\u002Fauthors\u002Fleo-martin",{"title":2834,"description":10},"authors\u002Fleo-martin","eYxCHkRgbGDV7shKdTA9s7Tu0zGV4yDGFoKR5MHQntY",{"id":3008,"title":2834,"body":3009,"description":10,"extension":13,"meta":3013,"name":3014,"navigation":16,"path":3015,"readingTime":18,"seo":3016,"stem":3017,"__hash__":3018},"authors\u002Fauthors\u002Floic-bousquet.md",{"type":7,"value":3010,"toc":3011},[],{"title":10,"searchDepth":11,"depth":11,"links":3012},[],{},"Loïc Bousquet","\u002Fauthors\u002Floic-bousquet",{"title":2834,"description":10},"authors\u002Floic-bousquet","ko12qZwiGL8XNjAoy9oWypPkIjr29Pbq7vhdtgldqeQ",{"id":3020,"title":2834,"body":3021,"description":10,"extension":13,"meta":3025,"name":3026,"navigation":16,"path":3027,"readingTime":18,"seo":3028,"stem":3029,"__hash__":3030},"authors\u002Fauthors\u002Floic-poullain.md",{"type":7,"value":3022,"toc":3023},[],{"title":10,"searchDepth":11,"depth":11,"links":3024},[],{},"Loïc Poullain","\u002Fauthors\u002Floic-poullain",{"title":2834,"description":10},"authors\u002Floic-poullain","oRIyJhFRTqxy5dLCYQ2OnYZ1DB-gLDUM-85vTSYuTF0",{"id":3032,"title":2922,"body":3033,"description":10,"extension":13,"meta":3037,"name":3038,"navigation":16,"path":3039,"readingTime":18,"seo":3040,"stem":3041,"__hash__":3042},"authors\u002Fauthors\u002Fmaud-lelu.md",{"type":7,"value":3034,"toc":3035},[],{"title":10,"searchDepth":11,"depth":11,"links":3036},[],{},"Maud Lélu","\u002Fauthors\u002Fmaud-lelu",{"title":2922,"description":10},"authors\u002Fmaud-lelu","MMbsCKuE41OMHusrl12FIEsI-Trx7l8Nn_ANhvj2_y4",{"id":3044,"title":2859,"body":3045,"description":10,"extension":13,"meta":3049,"name":3050,"navigation":16,"path":3051,"readingTime":18,"seo":3052,"stem":3053,"__hash__":3054},"authors\u002Fauthors\u002Fnicolas-poirier.md",{"type":7,"value":3046,"toc":3047},[],{"title":10,"searchDepth":11,"depth":11,"links":3048},[],{},"Nicolas Poirier","\u002Fauthors\u002Fnicolas-poirier",{"title":2859,"description":10},"authors\u002Fnicolas-poirier","dXrJkYo8az4SN_D23aYc3fQ7z8s1dR2a0lt1ogjAjJs",{"id":3056,"title":2859,"body":3057,"description":10,"extension":13,"meta":3061,"name":3062,"navigation":16,"path":3063,"readingTime":18,"seo":3064,"stem":3065,"__hash__":3066},"authors\u002Fauthors\u002Fraphael-sauget.md",{"type":7,"value":3058,"toc":3059},[],{"title":10,"searchDepth":11,"depth":11,"links":3060},[],{},"Raphaël Sauget","\u002Fauthors\u002Fraphael-sauget",{"title":2859,"description":10},"authors\u002Fraphael-sauget","Uri9bcq0QDuxRA0PbBoNtu7p_5L3dALu4kzcXVW0xyM",{"id":4,"title":5,"body":3068,"description":10,"extension":13,"meta":3072,"name":15,"navigation":16,"path":17,"readingTime":18,"seo":3073,"stem":20,"__hash__":21},{"type":7,"value":3069,"toc":3070},[],{"title":10,"searchDepth":11,"depth":11,"links":3071},[],{},{"title":5,"description":10},{"id":3075,"title":2859,"body":3076,"description":10,"extension":13,"meta":3080,"name":3081,"navigation":16,"path":3082,"readingTime":18,"seo":3083,"stem":3084,"__hash__":3085},"authors\u002Fauthors\u002Fromaric-juniet.md",{"type":7,"value":3077,"toc":3078},[],{"title":10,"searchDepth":11,"depth":11,"links":3079},[],{},"Romaric Juniet","\u002Fauthors\u002Fromaric-juniet",{"title":2859,"description":10},"authors\u002Fromaric-juniet","4Zb2artgT-eo-PHLXi3xi4d5t7s6PfhUxeSfXIikSUY",{"id":3087,"title":2834,"body":3088,"description":10,"extension":13,"meta":3092,"name":3093,"navigation":16,"path":3094,"readingTime":18,"seo":3095,"stem":3096,"__hash__":3097},"authors\u002Fauthors\u002Fstanyslas-bres.md",{"type":7,"value":3089,"toc":3090},[],{"title":10,"searchDepth":11,"depth":11,"links":3091},[],{},"Stanyslas Bres","\u002Fauthors\u002Fstanyslas-bres",{"title":2834,"description":10},"authors\u002Fstanyslas-bres","Xa0SahETuiN4q1jrmR2ych3moAqcZ2LbU7vSfEt2RuU",{"id":3099,"title":3100,"body":3101,"description":10,"extension":13,"meta":3105,"name":3106,"navigation":16,"path":3107,"readingTime":18,"seo":3108,"stem":3109,"__hash__":3110},"authors\u002Fauthors\u002Ftalent-acquisition.md","Talent Acquisition",{"type":7,"value":3102,"toc":3103},[],{"title":10,"searchDepth":11,"depth":11,"links":3104},[],{},"Équipe Talent Acquisition","\u002Fauthors\u002Ftalent-acquisition",{"description":10},"authors\u002Ftalent-acquisition","doDfE76txftQ4wIiKjJoDmSpyzSKk0tzlgVAp6-opAY",{"id":3112,"title":2834,"body":3113,"description":10,"extension":13,"meta":3117,"name":3118,"navigation":16,"path":3119,"readingTime":18,"seo":3120,"stem":3121,"__hash__":3122},"authors\u002Fauthors\u002Fvictor-borg.md",{"type":7,"value":3114,"toc":3115},[],{"title":10,"searchDepth":11,"depth":11,"links":3116},[],{},"Victor Borg","\u002Fauthors\u002Fvictor-borg",{"title":2834,"description":10},"authors\u002Fvictor-borg","-Za-JweoiP6hyclue_WkxMXdRUDTczPGlJf6AZckjUc",{"id":3124,"title":2834,"body":3125,"description":10,"extension":13,"meta":3129,"name":3130,"navigation":16,"path":3131,"readingTime":18,"seo":3132,"stem":3133,"__hash__":3134},"authors\u002Fauthors\u002Fvirgil-roger.md",{"type":7,"value":3126,"toc":3127},[],{"title":10,"searchDepth":11,"depth":11,"links":3128},[],{},"Virgil Roger","\u002Fauthors\u002Fvirgil-roger",{"title":2834,"description":10},"authors\u002Fvirgil-roger","DfVFe5j0bCgXeEr381ZYOM5DP4m-pWb93J9-m_muKJ0",{"id":3136,"title":2834,"body":3137,"description":10,"extension":13,"meta":3141,"name":3142,"navigation":16,"path":3143,"readingTime":18,"seo":3144,"stem":3145,"__hash__":3146},"authors\u002Fauthors\u002Fyukan-zhao.md",{"type":7,"value":3138,"toc":3139},[],{"title":10,"searchDepth":11,"depth":11,"links":3140},[],{},"Yukan Zhao","\u002Fauthors\u002Fyukan-zhao",{"title":2834,"description":10},"authors\u002Fyukan-zhao","LRPHugtAJnWHsmHxy9_SR5Zas_C5p-GR_uHEs1Fhk_E",1778159245014]