From f6f2ce539eb16127a2e2e734d92fb00fb08eb422 Mon Sep 17 00:00:00 2001
From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com>
Date: Mon, 24 Feb 2025 15:24:53 +0530
Subject: [PATCH 01/23] Initial commit

---
 sveltejs/public/build/bundle.js     | Bin 275427 -> 276854 bytes
 sveltejs/public/build/bundle.js.map | Bin 252546 -> 255677 bytes
 sveltejs/src/ProjectBrowser.svelte  |  43 ++++-----------
 sveltejs/src/QueryManager.js        |  82 ++++++++++++++++++++++++++++
 4 files changed, 94 insertions(+), 31 deletions(-)
 create mode 100644 sveltejs/src/QueryManager.js

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index 2dc635852ab161656e8e26b7c0bd72abc61f7084..82181ef54e9900ea2b53169a00406fe4f372db4f 100644
GIT binary patch
delta 3104
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

delta 1508
zcmezNN#OBwfemcsjE0-p%Uzg-4fPW9^72dbl2bKYib@L-b5sl`$5q{8G~8@ieVCci
zaP!}~M3(7!0*t(yOPlYpZw~D?V&2@@_ltKr|5axG$@kVPG;f>JzHJU;`?fhu+qcbO
zer7QJ!h1%J?LpU>IT@MEET_lbV7|?0vfc6~^D9OsL$m2Bx0#KYO=7L5e|*I(%ciBT
zU}S2xz2-La2}UL}i|KB6nU6DCZ0Ebje3OaU$lPeUTspHfPf>n(v6j9<a!EzB#dg7m
z%tp-2Mh0fn-JUQnnO^aUk$3y|C(Nac%!Wo5)AOD&J2IMY-|&oiCKHpP<#g|t%q5JL
z+Yh{CZe(P%oPOXXbNBSKubC^Dj4ZahzhOSa!e}x*{yVeSbcK)1o>2DohL6nK85vEc
zTYP4YXEfP9<1_PgCXm|=zA=X|noMu~#@r0zJ}6?AoIc?PGuw2H@62Wp&2}J(dEc2s
zcuZmyY;A27)ZL3RQ>IV&#w<Bq{0DP9NE)WkhS6mD*&oa+nT-t0rwcH%NN%6>lbMlG
z$<QEHhf6_0L4lLAIKQZ**h)cP3(SCqfuYg#Ex(vQn;OL`sHuVFN{f<HVUmUrd-b(a
z5=#=Hf=01=1v#bZnR%Wm(;wO}i){b&n|UGg^wmtveD)~HNH=i0!)j*9sppyFrZa{x
zOH7~3#3V4i;1v_c^ms;Q_Q|IY2~L;iVv?OM=gsW1m6L^)naSLEy8ttbFe|f>f$4NL
z4wh$(CewL2SyZN5++^kv0Yx>)zaSdwc#G+QoGjMU<!3R8O@Af8!Zm#jCyO(q#q<xH
zEZWRQre@RSxLBgl_|ntA2(V~Of5pWjk1SIq$WlE$pn-{f`Yvvkx1hLK&&wh<eIE~t
z0;9?F`#dbFj26?uLejh}Qq#k3Gb@8_01FlJvWPHSKvJ&cG(Hwq&fv@{P_Ubsfb^I!
znoW~u5}E#im4$aYD<6v_3n*l#3kb7F^XMo*{bOQmIo(j0#dLcGAImHjxC%LDQ$xe;
zhQcf>m>JC{$DNm-{Odf=^nbZbO4Azz7}>V#in9bUGn#F0kYaITVl>%)T!v*QBX>zg
zszOp?a&~H7%4GiuhST@Tv7|{G>nP;pC#GcPr7PGfloXYwYFcwa6Vvpb3(VF`oSf57
zUtrdmzG5Du^K{#b%xWy0oSdA~voA8MP3Mqh<eko!$;dW+<wa&06Hd-rgzn<h#G>Sk
zfW)H2++w}r)DjK#g2ePxbsYs25Uptqv*hnZW+_Xs7$|t);+Wb&NgAXbM8lO$54yyx
zA_z(R$@zK3B?_fQISRJZd9N_*1fZBvk{?`Bl$n>Vp;?;%vmBzMD79F@Rw1!GF|$M=
zEwv;$L!-1P2jP*-GzE>K)MCB-?CA=7nI)u+bQC}_1=j+S%PP*#)6mqMenE#tZ2G-h
zj9gqGC3-oT#U)U|3-T=dGGM`y{F20+pw!~hoRVUMEu7P@Tw&IRge~h;X4~l!kC=I;
zzr4l7KV5q-GcVXs=IB@?(-;k=^CvRPGa666ctLWzk^;+j7DkipQmQOx*cnZyKi6T2
pW;B`ZtjkitXtI5eE=w~bE694C>6v#KHKr%%vs~V8Z@^;C2><|D*^>YO

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index b43efeff6ef60e0e41ec035178f8fc9cb8272d0c..7dcb3ae346a366d37877cb93706b8625d3b10301 100644
GIT binary patch
delta 3842
zcmZqL%D?w4|ArtI{=m}IqDtSyyu|d>BE78Q$%#xN&5<nakt~ecBUzX}NeO1=7UUO|
zC{!yrIR?ANhX(mfU-*Vmsy;a<vA9?PS+_!UOrC;5a(-TMNl|HXNq&)rCPYX<Au~-u
zL$N$FFD1WR4_OObfr3IwMrN^Ia$<5us)DUTb*(kXECmIqcurz*30Pl1Vo_plv4X9F
zdR}Qx4qOoyqZMqyMowq+XA~92B0v417L!=LTWU#ihC)G6epYI7NwGp&QGTvMNk*zd
zQetv;YF>&$VqS_uaY=qrDo8X}Au~@QH#IlEs8SE?JS_;R;7|Z^mO{0E63CcZg|y6^
zlGLJN1zm-Zj8r(UBwr!1pdhCbWD3ZRqSV6r($wM-s9A7R{7Q3^Qj2O83KG*(p++Q^
z78RxDl|Y2@!1DNX1!q=)bV3YL$WK$qEJ@8RRwzg<0-J!)S`Tq|a7j^SUV5!UF({&S
z6@v4NN;31(6>?KcGV)Ups?imf7A1qMaY-ymR4C2|@!)EUQcFsU@)W8Ait=+ai&JeN
zA!Any^-oS_aS6y>e$bFt$Ong9F;u@kC|nbZEAx^Sa`F>XG@#+B19m-x1bI;h>|hWH
zvRhN3dU}vLQ`2;VRg7%YZ>uw%n7%}VNp1RR4JOU$Up1I)>kC0i9pYC7TZQ7(#G>R3
zNP^NU$%lw*Xj;eQfozAQSqKYPBDA$tKsHiM4W=C8TfOYm%3=*fx|Y<`%Sp{kFUe4_
zQ!s!ig{J?>52eKFL1AL00M%NSm{XdHtXWeBq63^I@=FqPf>Mi1b4rTgI?ybG*#*}I
z%Tcv3|A8Hb$XFtv*ahV*BrjP{uh(RfV~5&2-CUDN2RRUH6QHJp0uAIaa5gVWE!NA=
zhWZR73QG$L5FbXz>L`?;_`pB`!*SqJL?H!~vTPL+%M&w86hP)?6;HpQ$;3TfPK!xz
zx@9V(<n(kcCe?aWKVcLiAY=4Emg}YE7r7=TXJ}|Z)19V*tsSh4g2pmfcQjO9FEb@p
z!4_6NK#QANkar<r0`VFv*rAXDB`CF|C^NMzRUr|k3sI<mjfN(1uzoFgo`<BqT7}FM
zXbyooK*7@mszJd4Vi~NIsR0#$u>1s5onH^mM+*5x3Lt4vxto_?qL7wfng`3r;JlTd
zTB4De0*@o8$>0<Lc4Rcj-!(O0L%_bSjmZNCaye!hu3)Q>ms$?33G^Tp2{;?2R)8yx
zl+?7u(wq_nELyFn$5k;(ZBLMAy39Derj$`&y00RWhL(Z?w9Ww+`Ovxv5{&A4`pAOY
z3l*81Sb30zrW?Lu6ra92oKbN4Gc~51dQb|4q^2Tpj!}T6CuB`};9>?GCkhG*DzNef
zE&wWup=n(~K?TGEmp^cMP{9e8hZKcSt|KV1fZR}&Uk=J$5J!MhBRF6c6hIQr`K5U!
z3bvp$UYb*)hnzjEx1Z5sGMC`fQ9^d`b|z=0`St9fj*hO5I)U4lA7c8(HvPdaCb8*z
znwUAJ=ch2SZ9j05shf$}MJI5(?-?dDj_GWtnGB|XJH;e0JvNM)b9((zCf4Z}uP~hg
zDVaX^Dw87EfbEyBGX3LbcGbz9&KSomy#4t@CjV;&p3eHthE6)Zj*h-Mk&cd$I?j%c
z&eooe&UsEcu8xkbI^m9v;X3Y)j_x4A45#UhX-pzgU{w(HAVnZ4ONb%U7tCOmo$e68
z#41(-5z+B<bo7L1NOg2foxU)YNmww)8Db^aUXYd3CmJyePhX$P#LAd8z0r?JY`TvN
z6PF~4lk}Y+4smpJ_L$yS#AGf4aU$3dxGmEKE189-ujymrp1vi7NrJt=1>`)(>G>;}
zMCHJ$K+b^Z1lbId)prKFa{9)dOrq1xb~5o#_u9e4HQiw+6YKQbzD!)x+trwPrJZ%W
z9UZ+v>PuW8&IcRk@95}1-BFcUc)EWK$lK03fsT%WV6|ZPf|X8BjAs&2fyjV@3>+XJ
zqpF;BKn8%A0T7k`YnVi*EA%k2f^D6?zk-R2F>iWd4U@=p{T)o~(*xEpiA*mDU}ByA
z;WRVr^y-UDT+<u2GqFr>IK{-mo(J^`*pz?{Cf4cePB4kHS0D=$r};uD6YKODXP9}p
zoguDubaXD8em{yyIt(5y=pJ%(bk2Z;H#k5+zVZV53d{u?qXQ0pP)LH)7D$O9BsEND
z1O=jf1{2Tp1%*s})7wgz_%%WD6%ct4(+^@AEXa}|4gkf&bb|zDsp$zWOk5z{oT4Dr
zWze_)yB8GfXjasNO#`VcZ~;dH*o7c|5IFv<9UYy0KwL+N`$6`Al|VcJ$`)WxfW<)S
z${@iEHWm{3U^U<r3i2{YjWIM|I6FE{-+G2gP{7Q|(a{VkyG=JNU=rnmo9Q=wVm*`8
z^bHwItkZuwGjX$kOarHK0ar*e2Kfw}8>c(^F^f!pFqes)4{RIU`O_PNm_#{28o{ob
zUVn^9f+q*+Sdg!#e?H4>$nFYqUzPLpiC3BQreBC*Vx6v|!puMY$yFxF=@+_~Sf}%B
zXX2P{U%<q{2R9lVGKg%?Uf=`~VGL#xo&LU$nRUALP9`o9P>eys6jZoCB6RwNV@#sc
zK-T!QGjUG$pUlKI-64*NRRgRTmO0BH`3@W&U;-2?0U(z;PG7K@Nkjm|%s?v}lv%++
zGrjOKlL#wFK^RD_==9sin7F31&1B-AzG^uWZ(NQuI6OcKJm6+S6EHZmK#l+hEm$4c
zXAmwp%t5js*Hk&{R6032rb6N!BnnnEed7ZrF}6x4=Spwy?EyQO^B5Vkx9{J{{D+m%
zdHSCH%xfW3)AS9SnWd()9c0d9be(?S5VH=qx1%F8>3U8N6kwK{e(E6e?di)7F-t;K
zaKcqgcX-bvGQG}}nQgk<Qf98{ANZJAr~9igb2F7XO+RyxS#rA7Y-W+^7alW7Z1+3N
z+{MV0;kW(P5$2n$^`4H-L7-rA1i2eb_=3C{4CealB*W?hh_{^`oeQ9jas_Dwr7#^=
zM`s_MU`NMb5Gw>6!623o$OvChh&qA^>tZKI*C2>F@EkFnO_iB{`urqjHnw6XN9P=;
zX-3SVBH+C0?C1#9=;`RJ?=*el4Q7$)2iTa|w*NoJoXyFU<ug6!26GQbkxLZF1Fq9I
OiZF|97rV)v!w3M3GPXwm

delta 1399
zcmdmcmcMB$|Aruz<~)}6JQl|7c`QsnrKYdg#VESHRfFj=Bd3#Nuxos1kdMxE#q*59
z)3dag)TYOsXEfOUPK$|;m8&EpRUs)cIXg9Py3Z>{iRtnNOtsS;m6)2QuV2cjJN>;9
z(;`Nd=@XTi)HqcN64O(IGpkak7br7HaTMnlm8k0|s7!yL%w)@2T9l(;JH1(zNk^tQ
zHL)l;BOtLTF}GN+BtN*MC^IizL$fx)Iwo&=oGO!?d}&dRrgco7f`WoVW}1RVQEIVX
zezvAUHAF~Zx|AA|yuE@#QGR)`f~`VIVo9Q2PG)flR0~AHIlnZo1SVUOUy_&;lv-Sx
zQ&MallLs<qx~UqI(ezX`CWq-a3K%8Tp@v1rVl~A8Dq6ce#*oQeV*4Ubrg`;zu8xkb
zI=+sMzB(Y*^uPy9!rPNhGTmmIp4Q96KmEdFCbsDboy;85_b*~%o6eQT#I-&90#hRs
zlaBB9_m`N=I2awLueivhKYjW|CV}bK!kGA`7vE+&HGRraW|isu;Y_U4*F0sC+}?PH
z=?nLCt|iQ3+q+*d`CpsfAj>SmTj}I%;+Lr7?&#>Q<2-%i4<_kpX3Xr{nNBdfGP38o
zJ30pGxKB?MXBMCS!IqhIy1xgr^mKzDX4dUpCz*G#GP+K8J<q%rLN!f4=g2HIz5fDp
z9;5U0e;1f_7(KUZUu0%xoUVI`Sqdt~;_2w<Jl$_Dv-tLnmzXayvK9DeIO+IKx4*$G
zv3<c6=Dn;2*4Cbm?m?i?a0G>mqfVrwW2BD1qoco0s-t5nM8et8(b3x4*U`DcdHTWw
z%%am1447G`^Xf92x@0(m{Nt$O>FDSP(&nh+?da$YA%YzpgTV$^dpbIsIziNcrNKb}
zjs|Ct3orUJ2@5;wK&-L$baaK9I{m{zX3^;zN|{&{K+y#;6{HGDsksx_3W!or49H<q
z>gnigiBKpn33eL7Bu_`@JZBQkNjSu;?hkder=zPe#CS*`g8k<XN@id?TtR^o=;#;-
za(BA3j+qlk04xd)ZcrfTfaIqyxWFtb66xq#;tC2@cMt)Nm*DA!JD7y0>$fwrCOCrP
z%()C|A2{H__JK_Sdmd!6b*Yo1a}X%ZJrSmWSneQ4I71{r3PD_`H$Ym7oTfjx!6Z7J
zr<<9}9^`tkK^aau;2;BA<mu?>3DV^ZNh}~mATHQlu2oJTOCSkideBj3Lj_0cNJnRL
zCs4@vg48*K(uqGfv_V|=>1&TN%j!CU1U%u+az#}Hb)YrKG43TU(<d%s7M=e7Clf1+
zr=zpi^n_Susp&a8n7O9=-eBhB0U7IP4GJdLwCRltnB}+I-)2tZWGeEVzWfPu4@ZGZ
U6v#H$=?fn*iEa;g%ACsx0J9#Xpa1{>

diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte
index 77f62ce98..dcdec7259 100644
--- a/sveltejs/src/ProjectBrowser.svelte
+++ b/sveltejs/src/ProjectBrowser.svelte
@@ -8,7 +8,8 @@
   import { numberFormatter } from './util';
   import { updated, installList } from './InstallListProcessor';
   import MediaQuery from './MediaQuery.svelte';
-  import { BASE_URL, FULL_MODULE_PATH, MAX_SELECTIONS } from './constants';
+  import { FULL_MODULE_PATH, MAX_SELECTIONS } from './constants';
+  import QueryManager from './QueryManager';
 
   const { Drupal, drupalSettings } = window;
   const { announce } = Drupal;
@@ -62,42 +63,22 @@
   let searchComponent;
 
   /**
-   * Load data from the backend.
+   * Load data from QueryManager.
    *
    * @return {Promise<void>}
    *   Empty promise that resolves on content load.*
    */
   async function load() {
     loading = true;
-
-    // Encode the current filter values as URL parameters.
-    const searchParams = new URLSearchParams();
-    Object.entries($filters).forEach(([key, value]) => {
-      if (typeof value === 'boolean') {
-        value = Number(value).toString();
-      }
-      searchParams.set(key, value);
-    });
-    searchParams.set('page', $page);
-    searchParams.set('limit', $pageSize);
-    searchParams.set('sort', $sort);
-    searchParams.set('source', source);
-
-    const url = `${BASE_URL}project-browser/data/project?${searchParams.toString()}`;
-
-    const res = await fetch(url);
-    if (res.ok) {
-      data = await res.json();
-      rows = data.list;
-      rowsCount = data.totalResults;
-
-      if (data.error && data.error.length) {
-        new Drupal.Message().add(data.error, { type: 'error' });
-      }
-    } else {
-      rows = [];
-      rowsCount = 0;
-    }
+    const result = await QueryManager.load(
+      $filters,
+      $page,
+      $pageSize,
+      $sort,
+      source,
+    );
+    rows = result.list;
+    rowsCount = result.totalResults;
     loading = false;
   }
 
diff --git a/sveltejs/src/QueryManager.js b/sveltejs/src/QueryManager.js
new file mode 100644
index 000000000..fc437ebca
--- /dev/null
+++ b/sveltejs/src/QueryManager.js
@@ -0,0 +1,82 @@
+import { BASE_URL } from './constants';
+
+class QueryManager {
+  constructor() {
+    if (!window.QueryManager) {
+      this.cache = {};
+      this.lastQueryParams = null;
+      window.QueryManager = this;
+    }
+    return window.QueryManager;
+  }
+
+  /**
+   * Fetch projects from the backend and store them in memory.
+   *
+   * @param {Object} filters - The filters to apply in the request.
+   * @param {Number} page - The current page number.
+   * @param {Number} pageSize - Number of items per page.
+   * @param {String} sort - Sorting method.
+   * @param {String} source - Data source.
+   * @return {Promise<Object>} - The list of project objects.
+   */
+  async load(filters, page, pageSize, sort, source) {
+    // Encode the current filter values as URL parameters.
+    const searchParams = new URLSearchParams();
+    Object.entries(filters).forEach(([key, value]) => {
+      if (typeof value === 'boolean') {
+        value = Number(value).toString();
+      }
+      searchParams.set(key, value);
+    });
+    searchParams.set('page', page);
+    searchParams.set('limit', pageSize);
+    searchParams.set('sort', sort);
+    searchParams.set('source', source);
+
+    const queryString = searchParams.toString();
+
+    if (
+      this.lastQueryParams === queryString &&
+      Object.keys(this.cache).length > 0
+    ) {
+      return {
+        list: Object.values(this.cache),
+        totalResults: Object.keys(this.cache).length,
+      };
+    }
+
+    this.lastQueryParams = queryString;
+    const res = await fetch(
+      `${BASE_URL}project-browser/data/project?${queryString}`,
+    );
+    if (!res.ok) {
+      return { list: [], totalResults: 0 };
+    }
+
+    const data = await res.json();
+    if (data.error && data.error.length) {
+      new Drupal.Message().add(data.error, { type: 'error' });
+    }
+
+    this.cache = {};
+    data.list.forEach((project) => {
+      this.cache[project.id] = project;
+    });
+
+    return data;
+  }
+
+  /**
+   * Retrieve a project from the cache.
+   *
+   * @param {String} id - The project ID.
+   * @return {Object|null} - The project object or null if not found.
+   */
+  get(id) {
+    return this.cache[id] || null;
+  }
+}
+
+window.QueryManager = new QueryManager();
+export default window.QueryManager;
-- 
GitLab


From 6ef796c2878046e4d8bf2d5069736939e760acd6 Mon Sep 17 00:00:00 2001
From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com>
Date: Mon, 24 Feb 2025 17:05:09 +0530
Subject: [PATCH 02/23] Code update

---
 sveltejs/public/build/bundle.js     | Bin 276854 -> 277132 bytes
 sveltejs/public/build/bundle.js.map | Bin 255677 -> 255652 bytes
 sveltejs/src/ProjectBrowser.svelte  |   6 +++---
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index 82181ef54e9900ea2b53169a00406fe4f372db4f..15f43a01c00144dfd21a20d013ebf3cf64d177ee 100644
GIT binary patch
delta 944
zcmezNNucMeKtl^-3)2>6i<^ul+bwT0zhY!EG@Gt+o7srjB-VQR$5+g<Y+CvXMy6)l
zYi=`7WMZ_~&UcUbCKI!fxzTjFbY^LuqWtn=Eq#ULl8R`H?Sc=PZ!%A>_{7M&{reN<
zQbuM&Ba7*I&zKz<&9`rO#>~&mXu18sOXfyKM$733UNU!YKl_^5hLzD|di-~0vFQpQ
znLVNG?F}E9w=*)DOt<*V9M5R7ea2_z=}aJ}8GK_7VKkZE`i;36#C=f2EIEC`4`#OM
z8sC}CAe!w!67#+@hwzxhD%jfEDyX{`Wu{D@@Qqn=y7&*~c#t$qpADnQ_Om~jk1;A4
z8pP^wDJUo?aB>#s7nKxSDd=l~8Bos|8cpBwi}|yuQLKWR8d$EhC^;1-X$Y}NUn?cC
zBoQiT6suQ|Q<|Qc=b1A7p$)Uh_D{c=7cx&@&BV-SkD`op1E)KzW|o|Ko;hwhV+gav
z^tnt-0@Dj#F>y?fXJlrdeEN{!ba^f&+39lL%syK=S(rH)O{Vj5vZzeAxXH{T0*Wz^
z??5!vsTR`%Ia#cy%g<sGoBm3Gg=_j6P8MfIi|HRYS+tprOwFdtaj`_9@ujDK5n$1n
z{)&r59$BVJkfnNhKm!x|^j+L6Z$V+bo|i>z`aT{O1xAzU_jy=U87-!Rg`{~|q^5`6
zW>yB<02V6bWf5VvfFwN0X?!fKoWYq@pg=Y=0qHSeG@B;RBr^R2D+}**Rz4O<7Em}&
z7Z7HV=Fw4r`p3lBa=M`~i|O_XK9*T5a20aQriO;w4TV`&Ff*D@jyo?u`PX@#>Hl(>
zl%_WbFtTme6=w-zW;EO0AjRUw#AvepxC~1Slc2GVLQZ~SN@iZVf~`VHQEBS*`4)^~
z>_$2YV4FSVS=_-Q)9>A4<l+R0>E&b=mq5fW$g}W6?0+iHqRePK`Qio1?MezP-&q(<
zwo9q9oMC4)o&H>hC7RJ>y0b1z1*6IKJ-RH-jI1DYbf#zCWz?9SpwDu7yS)L6IVS*a
C$0t$%

delta 828
zcmeDAD)8--Ktl^-3)2>6i<^wb+bwT0zhY#xn67%8*=YL5JVue}etaza%!Wo5+iPz#
zPh?^=-_C!J`6d&yp|RO?xpZb}=A!)a=^y?vicFVeW)hfw;Tbc>^ek3J?(Gf_nO8G2
znr(mcgt?KC$;@PW!82w@X0uq$>4JgGGTSdaWB$m*XffU8HFNiL-&f4v7%jHDyk<Vm
z%4j^D=Oc4Gqw)5fkIZKo8I7k~eFk%9eP*7{#A2bKrZ#=wH)iSSf!~<T7>%cQd}D5A
zw3z;&h*@&F{zqmTUgKB=TU%QNb)U@Q67}gH6PaYE^WA0^h3M4#!7M%f;CHajAKy`Q
zvQ3}!gV~G^rro_LGeuo<dgDJvsp%UySS%<1dF?vg;R!R__PU?U(u|Cj(`Wx;mS;4a
zzWW#RXC_1Q>Fa+pi;5XQEZ5gcNi0d!(pN|>sfac--+uEq^M2;(ou16x{Af~OZA#d+
z#p)I0l%{9qd8TM8Oux&>q_~}vk%gU;(RjKb7mEs`(R4E|7CA=q>7iUK){LgpCvveQ
zF`7^3<6+STMcDM!+$_@5ZMj+O(FBndZMWfJ$p=Mv0w0Umbo1NH3XoXmyUolo{XZ`Y
z7o*8^u#^HHixi{LbOUx4c{VM51w$hXkhm73IY>yFPo*F+JvBJ9DpgBg!Pp2SB{uyO
zAB!BL=`?vJk?HgJSa_#%@v}%yR~BKB<knFr$}cb0(pNAt-0sWIa)AXTEW>PMYC7GI
zpT%^0xd=-!^YjVQEG&}~&&zD*l3;0LW;ESCQJTe#iP3ob4Ox~LCN3i#g`E7vl+3*J
z=@XO~#n_5ci%WA#rY9+|xKCeT!6+(V4zWhTRskw?R)IwertG%@i!!6p<ck+1w_7N(
zd}o=i|CW(;yNMdh33f)4>3?-uq8W{+N9wUuFdA>aq{q_CI6cvT<qHcaTDSWeve<J1
E09gb5LI3~&

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index 7dcb3ae346a366d37877cb93706b8625d3b10301..8dde87344fced52a12fe4d4a46a027d55e278aa7 100644
GIT binary patch
delta 248
zcmdmcmVe1v{)QIDEldjxSW*&85~r^*V5)<#3=Ekhz^ryZL#FM1hRi>wv*)@yItJ;u
zPfrwQ7N7pXmYH?BzX!AQbb}yf*6m#<nRl@=x=wdJ&%72wHBCR~$SgIz{{nL!qx1BC
z7npSzJ-2ILWM*cZu6v1D3M$6p>FDS@-ES_l`1XyLnAfv2rc7^Kz$^t4_MBdDi&>7z
z)M@&JTg>Lu58YxGoBsL+6W4T}Zf36OZ*DPjYGpX-1Uotg>$p2Qy6bp4I(q6jJ32b+
dL^?V~f>@4V!rIf(*?fBA8zz(O!MB;SxB-^LQ=R|-

delta 301
zcmZ2-mVfVA{)QIDEldjxCaXW-pT69HsamBdwYW5=L@y_^xWqapPeDOJp(ww+*g3y6
zuSCIC0V3P(WXQDL$&mTybk=NlN5`P)iJzIIrrUck%TG^u&cwRC?j-XrRz~OPmgkw*
zLa3(cdcn+M(?2|7Vwv82fjN)Sb-L{(W*tWF?eZ6ynHi@mUt*SoiV1i-Iy&nFIywgG
zxH>vI>I6@p_=-t*djA||sqHP7nAfu#1VJQxL9)J%j=nnXj*h`PAg;eovZG_9jx)#v
zPe(^j9cM@90_W*Px0ubjt&5!;U4wK$!qeAlF-uJE=VE4OD|T{p&T*O^zk^wH`sZ8B
dLX7&;?HHM*r%!mpBsqQmK4#YK_P3c+xdC%pW2^uG

diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte
index dcdec7259..b7f3e6a62 100644
--- a/sveltejs/src/ProjectBrowser.svelte
+++ b/sveltejs/src/ProjectBrowser.svelte
@@ -70,15 +70,15 @@
    */
   async function load() {
     loading = true;
-    const result = await QueryManager.load(
+    data = await QueryManager.load(
       $filters,
       $page,
       $pageSize,
       $sort,
       source,
     );
-    rows = result.list;
-    rowsCount = result.totalResults;
+    rows = data.list;
+    rowsCount = data.totalResults;
     loading = false;
   }
 
-- 
GitLab


From a036580085ca8620e6bdf346a6f97b5d15828732 Mon Sep 17 00:00:00 2001
From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com>
Date: Mon, 24 Feb 2025 17:13:39 +0530
Subject: [PATCH 03/23] lint

---
 sveltejs/public/build/bundle.js     | Bin 277132 -> 277132 bytes
 sveltejs/public/build/bundle.js.map | Bin 255652 -> 255609 bytes
 sveltejs/src/ProjectBrowser.svelte  |   8 +-------
 3 files changed, 1 insertion(+), 7 deletions(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index 15f43a01c00144dfd21a20d013ebf3cf64d177ee..c4192ca04f12c004538082fe685a414f770236ec 100644
GIT binary patch
delta 90
zcmeDAD$w&)prM7ag=q_O^bIBxlkKrLm|wCm8gF;M%lw~>(Qv!_6J};sW<yJp?Y=LW
we={)}Z})t|Y|6`Mw7vc(^JFd-6ALSo>4n=E#kcFSu>4_RG}x}i$&zFU0N?E%TL1t6

delta 89
zcmeDAD$w&)prM7ag=q_O^bICcgYB_5m|wCmT5fm0%lw~>(R{o66J}=CN#9wv`@LlT
v#l&d2-SZ8zDKDeN_WGa9let(-O{`3&7j9z|->%ET@`r`dY`YdGOOhb~0X!dH

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index 8dde87344fced52a12fe4d4a46a027d55e278aa7..e2b867ef1268cac89fd581707e42afbffced0e40 100644
GIT binary patch
delta 162
zcmZ2-mjCA&{)QIDElm3jG*!|vb4pT+iggrJ3KG*(p;T~Y6^L1!UsR%_P@G>{l$_cw
zY{;}-*pPYqG{)TRr;jst@h}<sZV$i0EWsw|=&Tdy=oqNu>gedI<L&6^J-ufRla#!#
zqoc2mv!kOUg0L=ja&)e6)$w$6^qhV_oLOReKNmCm_8YgDw{S8UdvEW1!YsxF0C99Q
AhX4Qo

delta 195
zcmex)hJVRf{)QIDElm3jLSph1z(6G}Gp8iAs8|OsP>`6OipmSltU?kg&Mzu~%NOUD
z7A3<Z+g}(kZGU0FynPyD<@N=~nY(zHGW@pdTw#`A({?s>(g}2Q4AgOTbPm$-c69Ux
rv3zub9UX%~tPq6QbOR%1QPyH7N7tb3owt~8a5822Y)^i|EXD)?0dYF`

diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte
index b7f3e6a62..c6e2fece2 100644
--- a/sveltejs/src/ProjectBrowser.svelte
+++ b/sveltejs/src/ProjectBrowser.svelte
@@ -70,13 +70,7 @@
    */
   async function load() {
     loading = true;
-    data = await QueryManager.load(
-      $filters,
-      $page,
-      $pageSize,
-      $sort,
-      source,
-    );
+    data = await QueryManager.load($filters, $page, $pageSize, $sort, source);
     rows = data.list;
     rowsCount = data.totalResults;
     loading = false;
-- 
GitLab


From fe1bb6585e4038bed400538369bed9a2b9008711 Mon Sep 17 00:00:00 2001
From: narendra-drupal <87118318+narendra-drupal@users.noreply.github.com>
Date: Tue, 25 Feb 2025 10:04:24 +0530
Subject: [PATCH 04/23] Feedback addressed

---
 sveltejs/public/build/bundle.js     | Bin 277132 -> 277781 bytes
 sveltejs/public/build/bundle.js.map | Bin 255609 -> 256236 bytes
 sveltejs/src/QueryManager.js        |  26 +++++++++++++++++++-------
 3 files changed, 19 insertions(+), 7 deletions(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index c4192ca04f12c004538082fe685a414f770236ec..be176a978374720459fd549681b4ebbd9d1d13fc 100644
GIT binary patch
delta 796
zcmeDADlqkzKtl^-3)2?nIcMwjwY0btKtM|&I5RIjC$%I$Pa!!cvA9^FD7CmCKd(45
zDJNASEx$-1Ewv;$BQq~uAu%sSp(Hi8AipTFC^M%Lraw6mB3)3FpOu<iqL7kUlBkeY
zl%K0ml98&Al$e~InwO%d;8~)OnOl&Pnwy$eQe3Z)s8E)llUS0Old7YToSIisl$euQ
zm70QXXl`n9Mq*xOajuR+YF=?^5y%}S8Hptdi8(n6nR&$}iFwJX#R~apV9NraPH-y9
zFE36lQpnF!NQ9YK4ECl%L1KEULUDaYVo|C>qCzsrqs1kuc_j+P`K3k4sbG_eN=q`p
zUPtzl9@HQxr4W*lSqu%af}+$ikgpVqQd3IvQWEn@6dVIQ6_OKka*7p_Diw-L^1%+z
z$xlp4O;IQ)%C857RIx&4o<eSFZhjFwEJ5K73bmBdqQsKS{5-I8GEyO~&dE$mO|DGN
zN!3&EOjAe%nFBILAtSL^Au*>YH8G`9At^OAPoXHaq$o4BEHy<3=Ge><h2+FMg``x4
z#N_1E;^O+$6otg}#LPT}vdlz<z|z#BO5eo1#Prl6z4X))jm#8Hh4RdjjQr9Pg}l_%
z6mY2LD-@*`mZlb$zzhXB6c)WuF({=Fl98$aN*AdL1*t{FnZ+f=3i)|CmEb@HX|qi(
zEh++qIwU$$i;F>?1tl<Wpr$4krKTv97Ubtad|s3a3OZN}=ue-1mf2?dm9xx>)AcVi
zvro@C$1FYBlZ9vc*R#x$(~n$Y7Mreij+uYD`8j5#?d|87(>R365{pp0sbV<oI<xq8
J*Xzvg$pDB75X}Gp

delta 149
zcmbRGOQ7egKtl^-3)2?nIcKLwUSKw7RGdEH0<+?D-y6(a(_da-=AT}5m04ms-$iEr
z>CzXORVI6~@JvqNVV}O^0<-k=={K0gwufG14&cx%&&*57FV_n!O)aYQP0UM7Pc2fg
eRme*%S3nlh(6pYu;3l(_Fc#(8&)j78Oa=hK%sAZu

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index e2b867ef1268cac89fd581707e42afbffced0e40..2651523b4eb321ed083b3ff0ba850423578bd6c9 100644
GIT binary patch
delta 954
zcmex)hX2h){)QIDEliW->-Dv?V)7KU6oNDJ(sNQv^79mua}tY-6^c@e3-a@dGm~;s
z71HvH6w*>lk~1>%(iIZ(QWQ#3a|`l|5{oi(D#4nQ6CrX1Mfq8&$t4OYi6w~&X+`<D
z3MCn-3Q38{*{OLcdJ3K;3YobDIjOm+c_qaPi3(-;IrWJpnK`LC3dyN?B}IujnN_JN
zC?@8nCTAq(Wfte^D5T~UmllCsP?C{YqL7%AqmY?bT#}fVoLa1qp9Z!m0BXNeQGR)G
zYLP;Io<btnxMHyH6bcg4Qx%Fc5{pt55*3m`-m5PzNzE%!D9$e}N=^kEQ&d`#0roMn
zXY@d(fUrVHMrJWIhzg2Q%fLP<N=+%vOG(TtQE&|KR7g(D$thMys#GX0$p^bPCqFSI
zHAMkxUa>-Eo<eSFZhjFc?CQ0^c7cK^rL-uqBr`t`?23$3h+}gy(^8WwlXFt_6g<-u
z5@CiYWF!_VB<2*QCZ<#>B&DY2DHNra6lJECrKaeBotaspkeryOkd&&Bn4FwiT%4Ms
zkeHsBnWs>enWzw0np#xpo0ylFo?2g|m!4Xpk(r{YP@Y+mkzZP(ke8a80uJVUg`(8L
z($wM-u!$fi!eSC60Ky6(8L0}O$WK)$NG&SPEG{Wl$j{5Eg!?JEw5SLazL0=VEiMN6
z50rwyftZ?DB$=9`P+E|m2k~uDDk!YLfvrD%f;^M4RC#7zN`AQ>l8cKp6{=(M6cnaA
zDKIIsDk#*(<W0XP&m=YdmpqfwbU6hk`R%C+Ogq?>QY#Abi%JwyQqvMkb3ooINA`G;
zhNkuOi{*?`?X4P2+gml5Ri;fBtYp%iu29J&GTp9{iEaA&ea!kszK)K*I?j%c&N`8f
zj*&X9j*hO@o{rAuPC8&2e@91u2n()edd_}k>FM#QOkC6N9A(y=Zn~dYd^&p<6YuoM
z{me4k7w>1D!!GIQ=<KLd>E!5`s^jVC=!r0Vy5e&tF}6x4=Ta}<=@*VOi%kEQ&n&wA
J^l|2Oasa1`OzHpt

delta 325
zcmaEJk^koz{)QIDEliW-r)wxN88RwPk5pokn?7HONo@KvH71Gar<Is^re9TJQk=Zi
zMPPcJ5|hOCZ%Rx%*e%O5^HTE5^#V&%iz<B+^Agijixg}X@>0tckcBiftz+_1D+=<9
zN)%F3(-KQ_N~YH;F^NyVT+S%b{z{8!`ztMGm1)!c4>FrhR}W+27m0LqjMQ;;bab`$
zbab|K((!e4^qqd;AhQyC36zt6j9GL#-yvpR_8e!3w9+AFCC04jjgy$frZ0?T65j4`
zh<O3KyQibGBUpisv!kQ44%lROM@M%ZPe(^j2p41#Sk~In(YeZ5r_#yMF%?bE^v0J=
cV!V}3&bi*+I$-_I(?70dR@iQHig|+^07N8c!2kdN

diff --git a/sveltejs/src/QueryManager.js b/sveltejs/src/QueryManager.js
index fc437ebca..bc411e9f9 100644
--- a/sveltejs/src/QueryManager.js
+++ b/sveltejs/src/QueryManager.js
@@ -1,13 +1,26 @@
 import { BASE_URL } from './constants';
 
+/**
+ * Singleton class responsible for fetching and temporarily
+ * caching project data from the backend. It implements a volatile, centralized
+ * caching mechanism, ensuring that all instances of the Project Browser on a
+ * single page share a consistent source of truth for project data.
+ *
+ * This class prevents redundant API calls by storing loaded projects in memory
+ * for the duration of the page lifecycle. If a project has already been retrieved,
+ * it can be accessed again via QueryManager.get(id) without needing to request
+ * the backend.
+ *
+ * The cache persists only for the current page session and is cleared upon page reload.
+ */
 class QueryManager {
   constructor() {
-    if (!window.QueryManager) {
-      this.cache = {};
-      this.lastQueryParams = null;
-      window.QueryManager = this;
+    if (window.QueryManager) {
+      return window.QueryManager;
     }
-    return window.QueryManager;
+    this.cache = {};
+    this.lastQueryParams = null;
+    window.QueryManager = this;
   }
 
   /**
@@ -78,5 +91,4 @@ class QueryManager {
   }
 }
 
-window.QueryManager = new QueryManager();
-export default window.QueryManager;
+export default new QueryManager();
-- 
GitLab


From 4b3b51f3be53f203e30543af0a5c683a64186937 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 25 Feb 2025 15:47:58 -0500
Subject: [PATCH 05/23] Don't be a singleton

---
 sveltejs/public/build/bundle.js     | Bin 277781 -> 277693 bytes
 sveltejs/public/build/bundle.js.map | Bin 256236 -> 256131 bytes
 sveltejs/src/ProjectBrowser.svelte  |   4 +++-
 sveltejs/src/QueryManager.js        |  32 +++++++++++++---------------
 4 files changed, 18 insertions(+), 18 deletions(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index be176a978374720459fd549681b4ebbd9d1d13fc..849bb486e71e692d87a9fb095b95e7a344ff330b 100644
GIT binary patch
delta 608
zcmbRGOJMI$frb{w7N#xC%g-u!B<7{$q!ufrrIsXTWagzSB<7_kl%(bs<QF9tW#&{)
ze|DBxefo>@%z{jr#nT1OF)L5EI>)TY&ZPhXTGOM?F>6ilImavy6WPA{9CH|>OipH5
zYOz9Qo<eSFZhlcE*x1Zsg`(8r)RO5|7np;l|G2=cJbn5NX0h#|7n!qJxbpLgONvU9
zOY)1R@4Lzzfy@`V#%zkj-|l{m`2-`9z;xal%x*~h?SVI#^-&bm-DFl54=hbBs`O3F
zOH5BK(o0TE&PY|TRj96=e&#0gX-4zumC4MqY?iSKwhEb(FMQNsHZn1r{_Ga>UncX|
z=?D86HMjHKVOC^fG~aG;k9j^Lqv`Yu_nE&inos+}=&+sjA@eC#M&s=lo-wnsGMa6_
z_L})O6Qk+&OYfLXd6`YDOr{rZV-(-6_=mZdiP3C&p(CTnwD-)c+fV#szR$vB7CYT>
zHM7QK{Syk)ycs2@Kk#7|21!q63t<+X9>>fqKYcbMi{Ny5E+*OOb-v8J+xuBqc$ipB
z4PXXuV`KTl!f3R8GdGJn8=I-Axq+$KHX)XaOuS}R3b~0T$r-7|3bmTkH;AyvPX8^!
z!pCZCWNv1$ollhI0V7oX_Ag>A?JNp~NTH-)tB{vku7DhH8q*zSF-mN2m1fCj=RsDx
Kok^KRSPB5`#?>kS

delta 626
zcmdn{Q()>Zfrb{w7N#xC%g@ROXXd5nq?Y98DJ17478ffNr4|?D=M`rr<)kX4<rhs~
zca~X_ol5}(w5A_F%dF0<;8`-A@f@=<TvX*8v*L80bIe+Bk+O5l^4sT}V-91qEJ!UX
z&MYn|R>;rGsRWs*P?C|VkX%|+l$uwfP>`6Os!*I-T%4Jor;wPJqL5jvkerj6Sd^MF
zo$ms3kbP#Ff<}2}UP^wsUSMfzQKfHUUSfJ`k)}d5#7!V~DHNralosVFU{N>Okwt{1
zBqOtUdgFCw(dh{nn3ZJ|uqjlqRRAmFzrifGedYz`Y?kQ)SDC}67hPpG-oEiF^D)Nh
z&DWV-wqL%^tjjo^=O(kNdRbzTLLicxR16ht74lNcQ3N$Kr+eIFKFw%0y)v0umdzqo
z!B!!2@`aBY%tj_A)01v7|7A4Wo^YGln~BkE`<lDV^BEb9r~BMz{=#Us-SGi47aODD
zcAsa=tgMVC+k;*+|7Kz|-tPB~*_4;r$jW4T;WkF`?Q?%K_cAe>PA_z16xnY7kNFA9
z^u$y~sp)n;%<9t@)G%>O|L?^pHT{7Pv+#8Lf6VOL7cjGwGqIRhzzi^AWBJ3vXs}(M
zo5h`t&B)xs)W~9cjUdZKkg0wmEat3+X2upK(;fL(s<*R>vZOM?6cvcEw6je2o5jer
UU0<3dpPdgiJhrb=ViA@C0G?~nCIA2c

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index 2651523b4eb321ed083b3ff0ba850423578bd6c9..a4780a86320d09caa8500b21ab15460611d604a2 100644
GIT binary patch
delta 748
zcmaEJk-zyOe?tr77N%M93Lc4hDLJXd3Tde&$r+h>=?aN?DGDX2xdr(}iA9+?mD8`u
zGpX~&<SA%@utG>iX7MxyCgths3QUSDAff3V3QStl%M_U8!QAar6_^|uWpXmhQi~Nb
z^AvJZbMuQT!KP*wD-@*`r<P3TS7LITzEz1yVf!~FCSw+^{Ji3lqSEA&{G#dV>P-H~
z{1SC0BP9OzCF)EI8Ic60J8LpIAn~_1YBC9+DCpN>lD7yfO)aYQP0UM7Pc70*PE5{7
zRj^g4uC<QIi^)r^D9A4=QAkNmODxSPQ9xE|J^f-iqty0ST1=(PiiJpKDA+3GrIw?p
z)zI7?q{noCg|TqEurbp*iS`TLOxrJbGuM|+Z>VNso$h~-*>rm8F=qbhe216?INco`
z-K`xRojs-(Rx=qvloXs~X4~F-i1|MgOSq$B`1HnfCei7$k1`7}6}U`af0Wr>5+tnS
z?C9vM<L&6^4Q5$;Iy&dMOlLjDtcTFW<?HC^3zErj-R^UY*^r43Rp)faNG9RwIXz4q
z(<km^5}Cf?1YGxhCei8k%a~ZFE0i&@PJgh3iFNvih0J_xAk*?(rVE^8o(!>P?n!1H
z6H8|uuunW4ojoAVaddQc0jYJ=iF9;~1dBl&3sR@!>geb?z40ZJ7+<B6vzb@A6Ii>B
z^Yo8vnH9F%oMP_dVJw<{`Wo{;Ik0wrM@N5%L9ULDjv&(<tvwxG^_{lwyUzTKg%vC}
fU2!Fo==S}0m=|*~m$^hum%q*|wY~m1b1owQ;p6x!

delta 831
zcmZp^$p7Xde?tr77N%M9^1+#T={cz-`FRS-If=!^3Pq{K1^Ic!nMpaR3TgR8(-+D!
zNwUP`DQHdKCC{YJtl(KP{hd6MGDJvFfk|<?nF5m*gqxtiB)`2`fyt54vLLmnIJ3B<
zSRp?zrxIk4LP<udLUL(QQEFa^LP27BszPyUadBpTo<d??ib7_wLUK-OVo_?!^aqMe
zZc&+O3L52^c`5njdV!^>MU}pZd5P(%MVboLF?kALP?TCyT9l`NMUgc`NiBpb$;d3$
zOHNGANL8>^sIIl1uAs!ECZm8&qk^phNcDC{B_<1&>8)x^zSHliF&S)^QfHdaIQ^&w
zlihZHO(s6Z>6%(hvP!8H1^GoK3Mr{+iKRIu3VEsJ3dl~?(6pX@v7AwA`wT6nQs(W}
zdQ6*H7z4MzH)2{R!Kb5yth9ZOH`Dex-pmc9(+jGZ*rux=WY)Lwb#(OAadvcc)`@g<
zjMQ;;bab`$baXa%(gDl(J39J9SO_&B7D!Fj^oDdM>FNBdm{_O#FJ$JOu3yQ-I^8y!
ziFbO~L1y{sHa$#SoFK(uqo+5{VG`!ichYfpbac0NbaeKZ&UlSUXL>?46ZiBBCz#o`
z?>@-<pK1C99cI?)_Ek)r(@Qro@w0&}s&JnE{0Or<Sc(f|AV{#xdAj>iW<9VVM1Aj3
zW=^I8m+do-G8-~sb=LRe%(m05s+hQ@zuw5iHNB>qnPd9@OeS`Ius4xRE;zwF8KO}9
zB(sb`jx)s7o{r8Q;J^a8-vvZC>Qp*8I;QG)Iy!oSy$KG%>47hq#CR*6oJ+lY!8&!E
zr*HhtB)@&tN#=eY#=PlP*O>opPrS~2n}yk5CwaTw9p>F!%tbCy)BCP5OKlf>&Ya5#
E0C^G&_5c6?

diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte
index c6e2fece2..c845f4bf4 100644
--- a/sveltejs/src/ProjectBrowser.svelte
+++ b/sveltejs/src/ProjectBrowser.svelte
@@ -24,6 +24,8 @@
     sortBy,
   } = drupalSettings.project_browser.instances[id];
 
+  const queryManager = new QueryManager();
+
   const filters = writable({});
   Object.entries(filterDefinitions).forEach(([key, definition]) => {
     $filters[key] = definition.value;
@@ -70,7 +72,7 @@
    */
   async function load() {
     loading = true;
-    data = await QueryManager.load($filters, $page, $pageSize, $sort, source);
+    data = await queryManager.load($filters, $page, $pageSize, $sort, source);
     rows = data.list;
     rowsCount = data.totalResults;
     loading = false;
diff --git a/sveltejs/src/QueryManager.js b/sveltejs/src/QueryManager.js
index bc411e9f9..be0388108 100644
--- a/sveltejs/src/QueryManager.js
+++ b/sveltejs/src/QueryManager.js
@@ -1,26 +1,22 @@
 import { BASE_URL } from './constants';
 
 /**
- * Singleton class responsible for fetching and temporarily
- * caching project data from the backend. It implements a volatile, centralized
- * caching mechanism, ensuring that all instances of the Project Browser on a
- * single page share a consistent source of truth for project data.
+ * Handles fetching and temporarily caching project data from the backend.
+ *
+ * This implements a volatile, centralized caching mechanism, ensuring that
+ * all instances of the Project Browser on a single page share a consistent
+ * source of truth for project data.
  *
  * This class prevents redundant API calls by storing loaded projects in memory
  * for the duration of the page lifecycle. If a project has already been retrieved,
  * it can be accessed again via QueryManager.get(id) without needing to request
  * the backend.
  *
- * The cache persists only for the current page session and is cleared upon page reload.
+ * The cache lives in memory and is reset upon page reload.
  */
 class QueryManager {
   constructor() {
-    if (window.QueryManager) {
-      return window.QueryManager;
-    }
-    this.cache = {};
     this.lastQueryParams = null;
-    window.QueryManager = this;
   }
 
   /**
@@ -51,11 +47,11 @@ class QueryManager {
 
     if (
       this.lastQueryParams === queryString &&
-      Object.keys(this.cache).length > 0
+      Object.keys(this.constructor.cache).length > 0
     ) {
       return {
-        list: Object.values(this.cache),
-        totalResults: Object.keys(this.cache).length,
+        list: Object.values(this.constructor.cache),
+        totalResults: Object.keys(this.constructor.cache).length,
       };
     }
 
@@ -72,9 +68,9 @@ class QueryManager {
       new Drupal.Message().add(data.error, { type: 'error' });
     }
 
-    this.cache = {};
+    this.constructor.cache = {};
     data.list.forEach((project) => {
-      this.cache[project.id] = project;
+      this.constructor.cache[project.id] = project;
     });
 
     return data;
@@ -87,8 +83,10 @@ class QueryManager {
    * @return {Object|null} - The project object or null if not found.
    */
   get(id) {
-    return this.cache[id] || null;
+    return this.constructor.cache[id] || null;
   }
 }
 
-export default new QueryManager();
+QueryManager.cache = {};
+
+export default QueryManager;
-- 
GitLab


From 2d1a5801dac91e9676b0540b7c60101041daf1cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 25 Feb 2025 15:53:21 -0500
Subject: [PATCH 06/23] Minor cleanup

---
 sveltejs/public/build/bundle.js     | Bin 277693 -> 277859 bytes
 sveltejs/public/build/bundle.js.map | Bin 256131 -> 256278 bytes
 sveltejs/src/QueryManager.js        |  11 ++++++-----
 3 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index 849bb486e71e692d87a9fb095b95e7a344ff330b..45add54fdd04e9dad3aa0f3a8d6c5fb20e6518bb 100644
GIT binary patch
delta 305
zcmdn{Q{eG0frb{w7N#xC8?Oi^=jRodC?qE)XQT#aR;4P~PXBm?S)1RQi%S6tGSd_^
zrXRS%tj>p|%uc~zy5?2pEN-|?9j?iVDdN*Jt~2vZPrbk_wEf#vW^3kpeSL+HjLc$%
z%wmO-j8ui<%)IoRRE6UF(xT*4h5R&ylA_X*4287(B89}99EF0S{H)aE5|G-&5`~P!
zvQ&kn)YLqMoczR;)D(rZqWoO2F-eKZ*{OLcdSFND>nnJcfGo&PtxQc(NUBswE6vHN
zR46P>%(2c)%S=synXBOGqN9+Qm!eRdkyr##3bHk@G_|PGH!&|UJ+(+7Gq1QLF)ul_
MSZ}+|P3DQo08wdcc>n+a

delta 146
zcmaF-OJMI$frb{w7N#xC8?UfurYUG}DJV?eca>RA!`4=zur#%(GPtBDGcR31O^r(d
z4*Zj{Qj<&cvQsOIHKsqj!7L`Ksh5+QmtK;gV5eXJS2*49DsvVOhGH(5VjZr@i7DdS
Q_g-a=X5N1NCUZ|R00r?eJ^%m!

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index a4780a86320d09caa8500b21ab15460611d604a2..673cca975220672e999c0ce9d551833f1487baae 100644
GIT binary patch
delta 428
zcmZp^$Up58e?tr77A74v!Q}kB;u3}A#N>?B;LNI21>5Pr)tIz-tz+_H@)Q&l6f)Bk
zG^RVKGl@<2Q)3boMp9{~V4$f`9g{ct;d6=UJJgt5IN_#jS5s$FV6NBKR|v_-ELO-Y
zRw&6xRVdEPOV3GFD9$e}N={YCPg5u<DlN%SNXsu$NX*GmC@9L$N=+^SsZA_V$Ve<p
zRY*!r%~Qz9PfSToQAjJw&jlNkl$e~InwO#%lc%q*;8_APJv+5BHANw*QX#E0C#O=O
zurx6zGcD61H3ep<f~Sj)LSkNuLUBf75kx7-%D~dpqDtSyyu|d>B8AMn;*!L?<kVul
zb{8$C?Jio(nzN>}RWJ!mUuDJ2HT{<l6W8<&N0>GEogE#WtsNbmvs|q`9i1Jgtzi<;
z(@Ax7Ox5vrboAB<cXSNb@pN?b)CqQU4AyaXbadANY0+_YbOh_rcbWe22y+@2NE&2<
ezSH&@N13-Vvw}E|(>+fzn=_hhZ#>D&Bo6@4D2y!t

delta 276
zcmbPsiNE<Ge?tr77A74vj?6R#jhH+Ih3Q)AOj6Uk)tCg?L0km|h3U3xOxpYkb_xa%
z0ZoPKn7qjkpG!<XuEyjt-A;{3i%SP0rJztdeWC`F=yYu*CZX-F>P+vM+TUt1ZGWr9
ztUgQ6+SAe5OUKvI(O1XQ(a}@K+0oHiC)Lq0Rma=W(OW0n(J>sP#LyY6W_m&ZljwB*
zT})il-yLNZp1vTIiEa9FZe~6KkfH(?up$uCZ~Dfg%xTjT1DHh6lyiaX0x8LG-L80y
pc^mWehbNhhxXrxMopjtC9o==Drze^*i%fqI%FMUj;S}>VIRLH_QWpRK

diff --git a/sveltejs/src/QueryManager.js b/sveltejs/src/QueryManager.js
index be0388108..5031ac3c7 100644
--- a/sveltejs/src/QueryManager.js
+++ b/sveltejs/src/QueryManager.js
@@ -45,13 +45,12 @@ class QueryManager {
 
     const queryString = searchParams.toString();
 
-    if (
-      this.lastQueryParams === queryString &&
-      Object.keys(this.constructor.cache).length > 0
-    ) {
+    const cacheSize = Object.keys(this.constructor.cache).length;
+
+    if (this.lastQueryParams === queryString && cacheSize > 0) {
       return {
         list: Object.values(this.constructor.cache),
-        totalResults: Object.keys(this.constructor.cache).length,
+        totalResults: cacheSize,
       };
     }
 
@@ -87,6 +86,8 @@ class QueryManager {
   }
 }
 
+// This is the single source of truth for all projects that have been loaded from the backend.
+// It is keyed by fully qualified project ID, and shared by all QueryManager instances.
 QueryManager.cache = {};
 
 export default QueryManager;
-- 
GitLab


From 07083b1d1075c7d47a6347e269d1eca0cceebe8f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 25 Feb 2025 16:03:16 -0500
Subject: [PATCH 07/23] Alter, do not overwrite, QueryManager cache

---
 sveltejs/public/build/bundle.js     | Bin 277859 -> 277822 bytes
 sveltejs/public/build/bundle.js.map | Bin 256278 -> 256193 bytes
 sveltejs/src/QueryManager.js        |   1 -
 3 files changed, 1 deletion(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index 45add54fdd04e9dad3aa0f3a8d6c5fb20e6518bb..d01e9d4bc98bd8bd156dc8b37b5fbce517f816da 100644
GIT binary patch
delta 21
dcmaF-OJLtGfrb{w7N#xC5!biZUT1!m3IKP43V;9r

delta 33
pcmdn@OW^S@frb{w7N#xC5!a_TUuPC$Q?OO2uC?Cof1UY#Dggbu4jTXf

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index 673cca975220672e999c0ce9d551833f1487baae..1722684dfd34abf2207ba44804b7186f0cc8fe18 100644
GIT binary patch
delta 57
zcmbPsiT~h5{)QIDEldkF+7D|mZ9lBR+&_PN>j~yQMs72&bSE8mM@M%Z=jn-N%p%ht
OgfjDOcR0m-O%4E%x)n13

delta 86
zcmX?jk$>7H{)QIDEldkFWJ)qJi}jN8^NLG~N|Q_Si}aEclQU8kY!#|&t=ng7Fm0c$
s!8~>T^fO19`KQl0$;>%DI+BTV`o10}j_qnEm}f9Dnrz>Hl9@>!0L5@1kN^Mx

diff --git a/sveltejs/src/QueryManager.js b/sveltejs/src/QueryManager.js
index 5031ac3c7..c295679c4 100644
--- a/sveltejs/src/QueryManager.js
+++ b/sveltejs/src/QueryManager.js
@@ -67,7 +67,6 @@ class QueryManager {
       new Drupal.Message().add(data.error, { type: 'error' });
     }
 
-    this.constructor.cache = {};
     data.list.forEach((project) => {
       this.constructor.cache[project.id] = project;
     });
-- 
GitLab


From cb5315d481701b57036c9fdf4772f94c154e3fb0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 25 Feb 2025 16:25:11 -0500
Subject: [PATCH 08/23] Remove one stupid thing

---
 sveltejs/public/build/bundle.js     | Bin 277822 -> 277678 bytes
 sveltejs/public/build/bundle.js.map | Bin 256193 -> 256044 bytes
 sveltejs/src/ProjectBrowser.svelte  |   2 +-
 3 files changed, 1 insertion(+), 1 deletion(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index d01e9d4bc98bd8bd156dc8b37b5fbce517f816da..c9a92266dfb13714f1cf51e6ececb75adf829fbb 100644
GIT binary patch
delta 304
zcmdn@OJLnkfrb{w7N#xC4_{AD*u*5iUFHq*Gj2xn=^s59MW$CJG73$9{hgVA`szeR
zPG%#E*h#M(CO3SQoBrShGspBfcbWOO@BG26&%$I9JH5`BMRGdxUuIiI!|9HHnWGua
zrqBJ$Y|Lml{o-F{8z!UJ>5bEwMW!qNV-}xY=E%rD{Q(mT-*lTpOg!7C{bOFtJl%0M
zv&7W%%yHA}{xS1xf5*fk!pvwk{cahf(sorgmYIx9hL+P0u(P;K-_Op%I+<mQ()0_G
z%m&+|Iasy{GMY@@dtPz6k0OiAc7G|B4UCLN+hka57#R(x8_Z&qm@XmD!m~Y5mSqa#
z^#9T<tkbvdW)|4aCC{S7#ArHQSCJ)w(O`R@B1@wHqv>`5BbJGb)Ag1!a&O;d%<`U*
M(RBMNQ<f?00Dh`wX8-^I

delta 382
zcmZ4YQ()gOfrb{w7N#xC4`0hDsHrIw=a&{Grz+Um+A5?ZmL%#G<dmjo=6R-UfB2f2
zm50${`bQ5&k?Fs`Gix(h#!h<WFg@Tcv)cB)AIx?vjHc7i{$UPhG@7pZmpPize0t+w
zW@AR9>0AFY+b|i&PH&vfEHa(_AG5f=z81t9Eq#ULl8R_U!&tC|`dT0>p@K%SaJ#2J
zv|$#R_J)~ly6GV%;qC7Km=`k}fgDSkIny0hGfPfA&m6Zsfr&+dnbCZEBP+{pMkXVJ
z>2~ZaE=-28lNq<DPQM_@Y_R<%JIgjfM$_#RBw5}vG8#|cFpE)a`xI#wHAY6G=?1eH
zC8pn(W#QR=UxsB0BcsXmjk}pews**}C^0dbO<%3RlE7%Vokx+SQGn5Gd!Hf8L`D`+
UsBRZEW?^AsG}|s^$})u=0DCQa`v3p{

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index 1722684dfd34abf2207ba44804b7186f0cc8fe18..9aeb065708da3bbb8b36dfae3e4aab2241233af3 100644
GIT binary patch
delta 105
zcmX?jk$=qv{)QIDElk}GthF(D3JTNZ#F%v3Z#pn-zv;mIaw40BlVf0o)AYaqX3^<I
zGnwUCGo2lS{ifGnVHTYpewq0oOTLq1!1RCsX0ho8Z<#ozE2uJavX(kI2K#NVzrws#
LXuIii<{m}>vFju=

delta 221
zcmZ2;f&bt|{)QIDElk}GG74&H3dQ-QMaiiOwzjqkDTyVCdIdS9>6v++Dec`3OxwF1
zn58GNdO8MHI88t3!Yn1=?C9vJ<Lv0@2q99Z_xLeO2q%LC+(A;Fj*iYcfsT%W(+g)X
zD@=cIiJ65d$9ekGOU&0)JRJj^b=*Np6F~%6Jy!Fyv6!dh>gecbjSNbi9D{S5x4*v3
Nyj5uX{AbL)i~udFL|p&?

diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte
index c845f4bf4..fd2ae675d 100644
--- a/sveltejs/src/ProjectBrowser.svelte
+++ b/sveltejs/src/ProjectBrowser.svelte
@@ -193,7 +193,7 @@
 
       <div class="pb-layout__header">
         <div class="pb-search-results">
-          {#if data && source === data.pluginId}
+          {#if data}
             {Drupal.formatPlural(
               rowsCount,
               `${numberFormatter.format(1)} Result`,
-- 
GitLab


From 69663aa78aea254929865db861457a0713f656fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Tue, 25 Feb 2025 21:53:59 -0500
Subject: [PATCH 09/23] Make the QueryManager class implement the store
 contract so it can update all subscribers dynamically

---
 sveltejs/public/build/bundle.js     | Bin 277678 -> 278654 bytes
 sveltejs/public/build/bundle.js.map | Bin 256044 -> 257584 bytes
 sveltejs/src/ProjectBrowser.svelte  |  15 +++++---
 sveltejs/src/QueryManager.js        |  54 +++++++++++++++++++---------
 4 files changed, 47 insertions(+), 22 deletions(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index c9a92266dfb13714f1cf51e6ececb75adf829fbb..93d66340ab1269f65bef537902861253c70acce3 100644
GIT binary patch
delta 2544
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
OFfp2JmojCU!VUnZcr5V%

delta 1615
zcmey@Ah_<QKtl^-3)2?n;0r7onhMp^6)!QHZdbX&{D*OS;5FvGOq2J<@(1N)7MEBl
z_$OthCYR`yCFYc-7HgDbWESfs=jRod6qP2I<QM5BCnjg4YU*$)Ac2znlEj>#)Z)^d
zl42_bu!`W!s#G1W$%!fA)1_`QACXh2R)82E9jl{&s@_1McKf}X%u<ZuwVKvkaD$6d
zOG=CK6jBmP60M<vwJ=$IEiI^!f|f#1YDrOMYFVm6qC!DYKFIM3X+`<D3MCn-5VuUv
zyuvIa9b8hBnU`LxkeQ;Os}Pcr3RmpuGJV2TW)VM#$7}LRb8>3Yl;nfML?ORO0VJ!C
znWm7JU!stfUz(Sq2X}}*l$D-ZqLG<0@uBeaL${d4r^jDr=HCA1HuE+{M)T>F$;`5h
zmeUX3W!7UdF`NGJF7s_hlj&#gF)K2e$4)=k&#1Xw@;>uCMkYhE={p`U8!?&0PM`3A
zS!#N|Fbg}gk*V2qfrreRjON>|9)eVvm`=a-i1|39#q>pwnROV=r+r~`*v|iiIfsea
z$lPdp-7{u2W{X(s=>p75lG_hFW0quQG@gFzCG(Q$6`vS+w|Bo{E@fmkG_sh^^oH4y
z**sQrx?mu)%=81#m^rp*y<sk55;U|lvobW$RH#<YOasTo^n^`J^3&}ZS=gqFzh_ou
zw4A>G19SKE6YrV7F<Nfl|ACpAk;%Yx`;m{#hgcX*rni4%j%PI4{`?#BSw=>a>05t*
zxuQRrAAy47qX(nN^mD(M*`_xnGV)B9`^_xLWDz^*mBZwQuX581{xFMz0%GlN<`7<!
zSOr^KTLpEW%;FOD$pI%NrmOv7jt5CUC}NhJKK~E14Wr5QJAat%Kw{E=nL`*&rWgEW
zZU%8-O4z2~`@w9+XfoaFAM<Kv6DyPHh1(d#r}O`3J_QQtGiEH3)4%*@mSHk9m@3F3
z&uBQ+kR_VYY-%ryF{9zsgDf^oMzPZyr!$KP87SB)=xc!zr<T4#a!EzB(ey$`Mv>_P
z@0r=ByE3w9Fq%OG*0Hg$Prv<&S!w%fMiysgUSut?(;ZhcOH4h_95>y8k%ebEZwNEb
z^gdRWRZM1JDUHecClsc6GfGZ>;KMAueH|Oi3r0q>>37Q*m8S3HU@2iXfcjv&J0}Yd
zBa3CMruB3wE*7cjg<LFZ%ti*WlNq<DPQM_@Y_R=37fT2;qtSF1K9*;UCeyX~SyUK}
zrw8z}$T3<>&*x{co-RL&No@K7eimOwi|Kp<EZX4YW+T87g~pej{y>1m9a(UOI7{{P
zfCeV^={E#f-hu-Bm=KE@C`3VNmpdmvF(orET}xlVV!E<0i^24dd5j{{6P_`1Ad+>Z
zFpCzG1vIhm7iQsMGBcTeR+z<v(QKML6IAtnVHVcu$|5Y1ETB-Ft|!SN&8?$QlwV$~
zrLSOOJUu~#MTO1O)ZD<-Y<rgo%S9$ev*|J7Eat4nM&@P~+h>Tg%wU9Tm18zFG@PC&
z!lDe*QX$Erz|3eedGC3}$<^m&CR?9ZWHg@~cV2$-uk$?9|K&0%O>YojWZUj7%VN#Q
zXf%DpEJm^IbLCjn7#R(x8_Z&qm_A>Tg=hK;d6vhFX4B_iXOw{md{JQOU^Je-aW}Kb
z^ofcr$01JeRbp|TzDb#-n9*dqnhMJTM$_rXRaoMgj19JHsIo*c3L5JuKtoQ!R-vS*
zG<EuX3q~<6BOP!)RIpW;K1Yqk2`o7M-YrHhPEerefl67h7>6MwXq=}T+-8<w(zKo~
zsKO#X`SJzv?Jnvp-&q(<w%h2ioMC4)oz7{(63u8bJ=KJzg3)C9JrkC0M$_q8rYzsV
UF5Mn!#xjA4(RBMO3zjMD0O6Yc!T<mO

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index 9aeb065708da3bbb8b36dfae3e4aab2241233af3..27b752d087632e7ced1d6082404eca48e3025de4 100644
GIT binary patch
delta 2233
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

delta 1029
zcmdmRhkwll{)QIDEle^>EE<{$)zdF1GwDu0YtF>AeTyp7MaJzL)tM$UO@8~FM<^$=
zxWr1qKPf9UxkRrlF{d<j`bAA9NfVuzJOwBy$uCLF2}&(4%_%9iQb<lr&PWZ;tV)He
zsI`Wut(|^OnMr<nwieS=IfZHkh{@5hItr);7%0?k(`I5}6sy&=j>&^)ElMpZEy`0!
zNi0d6e$bprtUf5Uq$o4BEL9;<p`a)q<T!=2qWoNil8jV{3-rJmwIHN|LqTFuVy;4U
za7j^SUV5!UW{QHYLP$m`T(hSORD*&8)UIlXpKJ0;b8>3YROefR!$TpzNC70RkeQ~C
zmtUfgmS38e0yRe;WOI6IiAH9Mrb0D{qo4pWdHMqtCgttVb(lW0v4A~v(U>W7yR#`1
zBNH2luV6df*__GGK*1L5Up-JbK|`=8zr5HvzcjA|CWjny+gF=2&E)|b*&gV^v^~&;
zS-z3W-O<tA+SAcF!)dzgF=mVDzm7B4Z9jj4S(2G0(9tn)x<L%H{IsXctkX@8GfPb0
z(80u_o#&zhlG2HEbd1z-b#!#q33hZ01{tF7q~q`C=&$4K=;#dM2Tgx|n)$|b*AvVF
zd|-uO0;F&H#%d;!=>n6PSeT5Rw;wsf+|Fp^?da$Y)(R%99UYwmK!!N#cse?ILJS78
zV5Vm{O@DoySyuz%29QU<8X;VeO(5N22;I~5k26~{SvpS-KhGS>WahPf-FfB$M#kLj
z@)w!^urj(%m$}Zo7D6>mf2YkXH9hSHvlpv{lVf1T^aDSc#HI@rGqX;=aD!Qn(Qo?u
z8_d^O@|_$5rWd9&i%p*(#w<Af!%b!uh?MAdu3OC87?})xrysh_tj$=o{oQTmTg;r!
zj*gDj!H&*e(+}Newq^Bnbj@;}u78kObbHkU<~fYh*9tRpO%Ff9EW!u&s=K3Ou#T^z
zqvLd`N6g;S7g#W}2^2dyI#;+tyan>~bj4yOVO~d&-yN+%k{M3Z1>%@RML^*U76lWa
wz%X~39(aUVYWw;}%-Nib#?z(WFn4lWhdY<KM1ezX`bJ4+(d~!dFy}G?0QM3~v;Y7A

diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte
index fd2ae675d..3a9ed182e 100644
--- a/sveltejs/src/ProjectBrowser.svelte
+++ b/sveltejs/src/ProjectBrowser.svelte
@@ -46,11 +46,17 @@
   setContext('mediaQueryValues', mediaQueryValues);
 
   let rowsCount = 0;
-  let data;
   let rows = [];
   const pageIndex = 0; // first row
   const preferredView = writable('Grid');
 
+  // Set up a callback function that will be called whenever project data
+  // is refreshed or loaded from the backend.
+  queryManager.subscribe((projects) => {
+    // `rows` is reactive, so just update it to trigger a re-render.
+    rows = projects;
+  });
+
   let loading = true;
   let toggleView = 'Grid';
   preferredView.subscribe((value) => {
@@ -72,9 +78,8 @@
    */
   async function load() {
     loading = true;
-    data = await queryManager.load($filters, $page, $pageSize, $sort, source);
-    rows = data.list;
-    rowsCount = data.totalResults;
+    await queryManager.load($filters, $page, $pageSize, $sort, source);
+    rowsCount = queryManager.count;
     loading = false;
   }
 
@@ -193,7 +198,7 @@
 
       <div class="pb-layout__header">
         <div class="pb-search-results">
-          {#if data}
+          {#if rowsCount}
             {Drupal.formatPlural(
               rowsCount,
               `${numberFormatter.format(1)} Result`,
diff --git a/sveltejs/src/QueryManager.js b/sveltejs/src/QueryManager.js
index c295679c4..ed45a1469 100644
--- a/sveltejs/src/QueryManager.js
+++ b/sveltejs/src/QueryManager.js
@@ -15,10 +15,35 @@ import { BASE_URL } from './constants';
  * The cache lives in memory and is reset upon page reload.
  */
 class QueryManager {
-  constructor() {
+  constructor () {
+    // A list of project IDs that were returned by the last query. These are
+    // only the project IDs; the most current data for each of them is stored
+    // in the static cache.
+    this.list = [];
+    // The subscribers that are listening for changes in the projects.
+    this.subscribers = [];
+    // The total (i.e., not paginated) number of results returned by the most
+    // recent query.
+    this.count = 0;
+
     this.lastQueryParams = null;
   }
 
+  subscribe (callback) {
+    const index = this.subscribers.length;
+    this.subscribers.push(callback);
+
+    // The store contract requires us to immediately call the new subscriber.
+    callback(
+      this.list.map(id => this.constructor.cache[id] || null),
+    );
+
+    // The store contract requires us to return an unsubscribe function.
+    return () => {
+      this.subscribers.splice(index, 1);
+    };
+  }
+
   /**
    * Fetch projects from the backend and store them in memory.
    *
@@ -30,6 +55,9 @@ class QueryManager {
    * @return {Promise<Object>} - The list of project objects.
    */
   async load(filters, page, pageSize, sort, source) {
+    this.list = [];
+    this.count = 0;
+
     // Encode the current filter values as URL parameters.
     const searchParams = new URLSearchParams();
     Object.entries(filters).forEach(([key, value]) => {
@@ -48,10 +76,7 @@ class QueryManager {
     const cacheSize = Object.keys(this.constructor.cache).length;
 
     if (this.lastQueryParams === queryString && cacheSize > 0) {
-      return {
-        list: Object.values(this.constructor.cache),
-        totalResults: cacheSize,
-      };
+      return;
     }
 
     this.lastQueryParams = queryString;
@@ -59,7 +84,7 @@ class QueryManager {
       `${BASE_URL}project-browser/data/project?${queryString}`,
     );
     if (!res.ok) {
-      return { list: [], totalResults: 0 };
+      return;
     }
 
     const data = await res.json();
@@ -69,19 +94,14 @@ class QueryManager {
 
     data.list.forEach((project) => {
       this.constructor.cache[project.id] = project;
+      this.list.push(project.id);
     });
+    this.count = data.totalResults;
 
-    return data;
-  }
-
-  /**
-   * Retrieve a project from the cache.
-   *
-   * @param {String} id - The project ID.
-   * @return {Object|null} - The project object or null if not found.
-   */
-  get(id) {
-    return this.constructor.cache[id] || null;
+    // Update all of the subscribers with the data we just loaded.
+    this.subscribers.forEach((callback) => {
+      callback(data.list);
+    });
   }
 }
 
-- 
GitLab


From ce897e19df91b2e20a82b4998e6da550c98b00bd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 00:22:26 -0500
Subject: [PATCH 10/23] Wire up subscribers to the cache

---
 sveltejs/public/build/bundle.js     | Bin 278654 -> 278684 bytes
 sveltejs/public/build/bundle.js.map | Bin 257584 -> 257897 bytes
 sveltejs/src/QueryManager.js        |  52 ++++++++++++++++------------
 3 files changed, 30 insertions(+), 22 deletions(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index 93d66340ab1269f65bef537902861253c70acce3..15bb8a34b665022831d92e776ae89bd9beadb2e2 100644
GIT binary patch
delta 650
zcmey@AULN{u%U&qg=q`(+_Te}?l1?4a49G#DCp}elw_nTBqb(ir{<;TO;5YSY+DbL
z2`o)5s`O3FOH5BKQpn6JE=kNwPA%30ODE^&6_+R^Cnjg4D%dJ0low@|BqrsgYE;*1
zT62L_q?P6+mt^MWDWs>CxE1B+I)l|}6cpuWr6!jYYbsQOl_@A78Re2#lBiH`3(^u_
zT#{du8ef)}Q<|y)wnx(%s-Y;gq_ikc0cH|ZR6#*6H?cq?GeyDH4&pG7Y0;S}v6=`K
zX_+}CsYM!@C8@a}WhIpbsrhLN5U#DQf_i>ZRw`?9iMpos_6cX1?HRYfI?wFDl&Y_<
z5T22mms*xu1P(8-dlZs067$kii**#rQx(b+^GXy-@)gqZi^>y=QWQ!u5=#{F^FTuR
zr9}$GrAfufMVU#dMbPjA1wPmmJ-F25jTXXMkidX>O#vJf@X*sJ$;d3$%gHP*(X{58
ze&;f?%=Fc_nUyA2M|1HZ0u~fZlN-xKm})h*%U@#VViZS?0VFfFmtJOOWu1QT2D8TY
zUpJV~GEV=0i#Y@mc6y}+DTyVi8jzp|g`_4Zz(8>}z5f=oF}DIl9Y|*Sja$r0lRL|J
bgcKB@aRc@mSndJHf&U+|ZGU=)d1f*Ia~a-I

delta 678
zcmbQ!DEO~Iu%U&qg=q`(+_T&L&NCY^)@K$gB<Ca+7b_GLrIw}Ul@u!!rKXhTr6lH+
zC^!aqDkLZ7<P<9;RVoyh<QHY;r7Ps*C#IyPC=?XsXQd{W6f0!rDdeW+<`-3RDS&{M
zLRx;2LP<udLP}{-Vo7Fxo<e>am|u{Xo~n?OnU<PdnVgfVr{I~Ukf=}(HAf*Mu~;E7
zrzkZsrBWd&H8oG6D7B<0Gqo%=MF(bZW{E;_VxB@$szPFNa%ypLYKlT)dSYgtLRn^_
zLSSiXQKfHUUSfJ`kzRUgiAH9Mrb2mUNk)EYi9%j#Y6{5hCHV?PsfDGf#U(H^xj`;X
zN=(j9%}d#ycY!&GY5L8}%>P{=@*oc>lw@QU>*Zt?m+0jt7HDLqDA?LT#FF##ic5-0
zlS}f8^pX>kGg6~7Q(_frY83KHb8<9wxF#2Li%hS*!mPSo;VSbL78|g71+dEC%&Jrc
zTLu3lP#EfEr&bngfUN;5K{irTFDErGy(DA$%bU!`f(mMCNc!y*47SVMVm`|#h@oxz
z>^sa#)1TaCj<nO)R|qXgNi0cKNX*HB#YJ&xQgL!oW>RWVF*uOG5tNcxlBiIgs*qJ$
zTmp?|z3E<8n5CxIU1nAi2Bid0NNIr7fdfQy`fZRkbI&pdOrLa~S%^tre>&qiW?NH~
k*igvKD=taQOHM7;1BWU~Oo2l~!B(NV)_VK5JIu?H0S^cA$p8QV

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index 27b752d087632e7ced1d6082404eca48e3025de4..e61aa58662d0e48ccd6de2f4c88b98952378dac5 100644
GIT binary patch
delta 1297
zcmdmRhyUe0{)QIDEldUS)6;z!^{4OgW#pJ%A<rZ|{i_aBfM86XzP>_9Myf(mVsdtB
zUW(rIFkL3wda!U{X=+iWZ(?3zdTNnEW?pegVqS7;v0hAGa(-TMi9&K>az?6xt%5>%
zQD#YEQckKyb*-j#OkPZ0T4`Q#NoIbYLV9Y6TTy<lGgzfYK~a8IYH~@jrb2a0o`M3B
zDK3d6i3+wL<r-j9(?RAaC={iZlosVF!1RGMDJbaWCKhO9rYP9jL0kaR8=aXFs|l4(
z%giZBEz-y=NzDbRDXA<-%}-N+aBXcB)bo=-j!*~LP`kZPo=J&u`W{6lTana?g8ZTq
zg_P8^#L}D+h2)&X;^OVxN=&C2>-F^&!ZT9yQp-|{z+nY;qe5~<VqSV`v5rD{szP~U
zUWr0UzCv1lQF&rfib6?7Vu?b29!My^v`C@2G^sebC^IRw2o!t@3JMUX>%qk}G&Dhu
z1&0e*8XUq<k1G{J!Ui6c8YLN-#d<lJ#U&tLfc!n(QH4o#`WhW3c@2=+IXR%{L<G1s
zNDatba0EdZ)6Xh1{SZfvAtYnBbE`6~VwwI+lSyOxdo3o#>DQE*#J6W_F=aDO=hk8J
zQ3MBpUTHx}Vo9n7B+@_;f$+ujrP@q7+#ts!XQYCpra#walGlYh03N$wlRyffj)bbH
zjmfK>ezBZUs{Ow%)As+m%<R)yES(LVraK;G7UlGIboAEobaeEbe&8^Zl!&9Fv#FD{
zr=zo@POzh6uui0-W90P0*-XNcS<X7Hj*hN6zK)K*5IJ{8M|U@!)aeHkn54D!ogmWw
zj*k91fsT%WAVad8bin3=RO&c8Izo(?Zm^I^YWjo<CJrWZr|E@pOrp~#L^4V8gB5@X
zkXB3Q>51o=C8r;_!K5UZs*~*mawkl(XnM*~=AF~epJ3)@0a-9Tuz*QK6r|123F1Jo
zD?!efzOfP%p9h#kxEvjw^_@YA9H;*~%Pckh`#EMA9&=}~Yr(|yfCWq<dLR|XPC8(Z
zfGh+%93&V5@eD%D(a||z`p47E^3%<aF&~@0FcD&t@bvppOss6yj*iYH&fBYxGaqE0
zKIb&E92Y43Kn7VlZ@+Px*^FtrPB9bL^!U@vT+<(%W#*c$bB>t{6fT_8{koaNrY}0d
zEIHjKnu&9|R5}ya^t0!fxtVl4r*AM~mYCik%gibQjv<i$L55~HgMB_-v6x8|WSk67
z5!7oSWzz%KF^Nn!IL9o(STX%$Hj{{S2v|A9n;;c%Z<aYtPfTYL<_^#SMH|RykLitX
VnZ(#Dot*PMOP#lGIM3W52LLjfpAP^4

delta 1164
zcmaEPkAK4*{)QIDEldUS)91@GDR1{yWRhj9&n#9*&PgmTRwyV+ElbTSDOM;-O)1Sw
zNz5xza18KNNKVYjDOO0TR46XVFUrhISIEgvOi4{qC@9L$N=+^)R>;g#$W6`7FRF~m
zQ_xaK%P&$W$w*a5DJ@DY$;{7F$WH_F3lh^)6>>7uQj;r_b5ivbJkt~sL1xs03{l8P
zELKR&DN0RDsZ>ZxP0dp%N-ZhMOf5@I(E*#9S)!1fn5U4Gs*sqRoLXF*nxc@Ho|u`Z
zP?njf5LlX8ROy?TmzbVfq?evrqLG=RsZgF-l969pqL7!GngVilNxni+YGG+=aS7N!
zkQ0*<le1H&Pg7*#o1US>WGe&-24vH=?^I$s%{aYFmFc}(Or8Q5lw@QU>*Zt?m+0jt
z7HDLqDA?L5fW?yY^NLG~N|Q_Si}aEclQUAIGgD#}YHAenN^^2FLH<xsP}n{}jcFB&
z4Op!LSXFRlRjPumf`1YyAoQ|RD~mO-8>gw4lbV-ak}=&-o5@g6K}`)wzny}?bU7U+
z#qG`7Oihe}VB?XUGJT~kll(MYra%XMeTC41l*E!$g~Xg3SmYF!CKV?aWhSK-6@vo~
z9AhboC5Z~<sR~)8#U;>4*Ne%UuByspq6$hBpy1O0DFTOvrZpshY9Wl;n7rEQd*zuz
z1;J*4)TZX8=*8se>rYo!V6qKDi8Y1HyyB9?yyVnky_h`Y2!I5nf~`Vzt#wRZOkQe5
zL4Hw*LP}~{VrfnZiW%0^FP1Y(we#ySZRgixW}nWY?`-Ha{ooBI!|4<1nM9|ruV-T8
zi*$61)Nyunbhh?%bk3XJdW?DJbfXi@4_HC`sObV1n5Cw_k7weV?y!f6b$Z+dCP_|j
zM@Mh4mg%{Bm?YUi3e8=nzrM>PzdiIM^G4?BZZ%B&EWVD8zSB1zV3J}9cXSM&Zn%g^
zbh=IqGY5~ewWFhRmMch`<MhOH%%amp&ocANc{)0J>I6GF2J5&xI=X|+*Ku`p1gq0`
z*{*k%*^CKdl+-pRsp)2UO#J-*j*k9dl_10NTp+HUaGaTI`uYn@T+_|#nS`dtb~A}j
zUwxk09PA^<>9H4>q^3VR#mp}f=;#=z1M&<=x3RMh*u{+%Orp~#9B1Zd^>hRUS7ADn
zux$X?O(10+5bGQron1g&M+Bh*4@t0@U>#r&OrQ9cNsO=3$=Sj)-3cP>Jbhy|v-I?U
OuT1RQg)TCO%K-o~^nwlm

diff --git a/sveltejs/src/QueryManager.js b/sveltejs/src/QueryManager.js
index ed45a1469..2c8a0390b 100644
--- a/sveltejs/src/QueryManager.js
+++ b/sveltejs/src/QueryManager.js
@@ -1,5 +1,18 @@
+import { get, writable } from 'svelte/store';
 import { BASE_URL } from './constants';
 
+// This is the single source of truth for all projects that have been loaded from
+// the backend. It is keyed by fully qualified project ID, and shared by all
+// QueryManager instances.
+const cache =  writable({});
+
+function getFromCache (projects) {
+  const cacheData = get(cache);
+  return projects
+    .map(id => cacheData[id])
+    .filter(item => typeof item === 'object');
+}
+
 /**
  * Handles fetching and temporarily caching project data from the backend.
  *
@@ -7,14 +20,9 @@ import { BASE_URL } from './constants';
  * all instances of the Project Browser on a single page share a consistent
  * source of truth for project data.
  *
- * This class prevents redundant API calls by storing loaded projects in memory
- * for the duration of the page lifecycle. If a project has already been retrieved,
- * it can be accessed again via QueryManager.get(id) without needing to request
- * the backend.
- *
  * The cache lives in memory and is reset upon page reload.
  */
-class QueryManager {
+export default class {
   constructor () {
     // A list of project IDs that were returned by the last query. These are
     // only the project IDs; the most current data for each of them is stored
@@ -26,6 +34,15 @@ class QueryManager {
     // recent query.
     this.count = 0;
 
+    // Whenever the cache changes, we want to forward that on to our subscribers.
+    cache.subscribe(() => {
+      const projects = getFromCache(this.list);
+
+      this.subscribers.forEach((callback) => {
+        callback(projects);
+      });
+    });
+
     this.lastQueryParams = null;
   }
 
@@ -34,9 +51,7 @@ class QueryManager {
     this.subscribers.push(callback);
 
     // The store contract requires us to immediately call the new subscriber.
-    callback(
-      this.list.map(id => this.constructor.cache[id] || null),
-    );
+    callback(getFromCache(this.list));
 
     // The store contract requires us to return an unsubscribe function.
     return () => {
@@ -73,9 +88,7 @@ class QueryManager {
 
     const queryString = searchParams.toString();
 
-    const cacheSize = Object.keys(this.constructor.cache).length;
-
-    if (this.lastQueryParams === queryString && cacheSize > 0) {
+    if (this.lastQueryParams === queryString) {
       return;
     }
 
@@ -93,20 +106,15 @@ class QueryManager {
     }
 
     data.list.forEach((project) => {
-      this.constructor.cache[project.id] = project;
       this.list.push(project.id);
     });
     this.count = data.totalResults;
 
-    // Update all of the subscribers with the data we just loaded.
-    this.subscribers.forEach((callback) => {
-      callback(data.list);
+    cache.update((cacheData) => {
+      data.list.forEach((project) => {
+        cacheData[project.id] = project;
+      });
+      return cacheData;
     });
   }
 }
-
-// This is the single source of truth for all projects that have been loaded from the backend.
-// It is keyed by fully qualified project ID, and shared by all QueryManager instances.
-QueryManager.cache = {};
-
-export default QueryManager;
-- 
GitLab


From a95cf1a19c639aee159a0593305b93e3d3ad3a4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 07:47:14 -0500
Subject: [PATCH 11/23] Add updateProjectsInCache

---
 sveltejs/public/build/bundle.js     | Bin 278684 -> 278768 bytes
 sveltejs/public/build/bundle.js.map | Bin 257897 -> 258059 bytes
 sveltejs/src/QueryManager.js        |  18 +++++++++++-------
 3 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index 15bb8a34b665022831d92e776ae89bd9beadb2e2..ae4fa03654677c45191f17a8e7f4a2e8ebf00aac 100644
GIT binary patch
delta 152
zcmbQ!DEOgKu%U&qg=q`3@A=8=S~(}5GhverD9X=DO)e?+%yUjm&PY|zD1ZuRDpYe#
zH^0NI!vPbSo_B{?Y5Jr)%$k!s%Y-?TLF!!+OA;rqYZ2e>d!G3Nk49-hN@7VWnwc6Y
di6x18Ihn;Jn$}zjU@-aPBa!XQcbR7<0|0f6IiUam

delta 55
zcmey+C^)B4u%U&qg=q`3@A>W87nncrOs_xBtTuh>9cJC>XU{WhPi`;co6dBBS&CUf
Mp?15~UFL1c0ECeiH2?qr

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index e61aa58662d0e48ccd6de2f4c88b98952378dac5..d18671ad2b11dfb6b42bef01256ef7fbbe6c777e 100644
GIT binary patch
delta 316
zcmaEPkH7l?e?tr77A7x6$+XhE<dV$%JcZJNl*E$MfTH}Y)Z~(4&%EjD6qv-P2k9~?
zaTGuWr$5wTlApdnmr0GaR?|8rZ~A#%CP~KH?T(5}0zC5Q1~?}sXQXPRB$g!V<zyC@
zw7=G6+WuOXId;bM^Lv@t*b1E-9SfbNH_m1fW;b-!admWbojy^7S$Mkt8zy$iU`NMb
z9dAcRZ);CSXG<p?Pe(^joj^y&!086(nU&acoOOI19et<!USKw4Dsi4(n9d|To#QAI
z4?kF=v!kOkNMn)H^o5U^gs1mjV&dQKevEk`=XAYBCb8)gjx+O5e^A84!tJc%?&#<a
oQtL5&;#(#$zDg(Obk9<Vu#WTejn&N3(*wRTv2Pc;$Q&*Q0K=YWb^rhX

delta 236
zcmeBv!2j|de?tr77A7yn?Se{7>^##e6`7Q$&(&qpnto7`NrO{Cp;ps6CQm_O`a4A?
zNoECw+IDk2rtRi>%&{}J&pOV$fOGon6U<!GV|balraM?Nvx)>eItJ@_J34w>dpbI2
zIP3U2I{Hq3|Aa|&y8byP)@c`+IHym$z|6-}<g63u=oqL2Qa{~sDwD``fmSBA>HQH*
ztkZp$F>|tk^jA1f|5(T*EFA(iM#taL(I2cq$Jx=*8Kj`hX}aT4CQ(jzM@M%M-(&j0
bH%wyel}^t2o~6#y1urp+Y@dITIY$lvX6#5t

diff --git a/sveltejs/src/QueryManager.js b/sveltejs/src/QueryManager.js
index 2c8a0390b..339fb6253 100644
--- a/sveltejs/src/QueryManager.js
+++ b/sveltejs/src/QueryManager.js
@@ -13,6 +13,15 @@ function getFromCache (projects) {
     .filter(item => typeof item === 'object');
 }
 
+function updateProjectsInCache (projects) {
+  cache.update((cacheData) => {
+    projects.forEach((project) => {
+      cacheData[project.id] = project;
+    });
+    return cacheData;
+  });
+}
+
 /**
  * Handles fetching and temporarily caching project data from the backend.
  *
@@ -34,7 +43,7 @@ export default class {
     // recent query.
     this.count = 0;
 
-    // Whenever the cache changes, we want to forward that on to our subscribers.
+    // Whenever the cache changes, we want to notify our subscribers.
     cache.subscribe(() => {
       const projects = getFromCache(this.list);
 
@@ -110,11 +119,6 @@ export default class {
     });
     this.count = data.totalResults;
 
-    cache.update((cacheData) => {
-      data.list.forEach((project) => {
-        cacheData[project.id] = project;
-      });
-      return cacheData;
-    });
+    updateProjectsInCache(data.list);
   }
 }
-- 
GitLab


From 16ce3bf0da418e7b1f0d2d1cd7670312bfd2a534 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 07:52:31 -0500
Subject: [PATCH 12/23] Add refresh_project AJAX command callback

---
 sveltejs/public/build/bundle.js     | Bin 278768 -> 278887 bytes
 sveltejs/public/build/bundle.js.map | Bin 258059 -> 258319 bytes
 sveltejs/src/QueryManager.js        |   4 ++++
 3 files changed, 4 insertions(+)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index ae4fa03654677c45191f17a8e7f4a2e8ebf00aac..56b511bb69fe5939aa8f175e34aa259748724427 100644
GIT binary patch
delta 166
zcmey+DEPcdu%U&qg=q`3_Jw+vqSAuI96iUZ#0uy9+}y;xlw!StqWqHllFEWqy`t2#
zqSWGy_=2MRtkmQZ1zQD;#H_>$9fhLQ;)49V;#5rqTRVknE(I_sEl5c$NezIgE%wZF
yPE5{7)rf|v1(^#nC05fKtfAJLi)(UQKmT^K3(S+4+4AyBGSe!z-@42!mJ0v}yF26n

delta 46
zcmaFfB>16Gu%U&qg=q`3_J!^1E-=q!7D~%6Do-p*Q7FksEK$hMQz*&b9(aXWEEfR5
C@)Aq{

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index d18671ad2b11dfb6b42bef01256ef7fbbe6c777e..d96ebd864fe3e5eac3aec7f841509642f6ffd99a 100644
GIT binary patch
delta 277
zcmeBvz~BFfzoCV33zMKyy-QJPL1K=cV^(5?bAE1aVqQwIUO`cQNq$LXL8@L+YFbfh
zaYlSWQGQlxa*2Yif<|IiVug-EQEG8PeqM2^rh=`VLUl}@f<kFQN@7WB07PN2XP$Fn
zaz?5~G)y7LP>>O^n$|ISwbn6tF?sr0S~1&2m6#Hk+4AyBGSe#C+f|vix2rM>FPdIo
z%fvRlD4j{tGS$&BRma`Y(Ot*c(a~AQ)6vmW$JNo%RVUEVF;K_b(b3!5)6u!W6(U&Z
z<mg!F1XenI?*%5Q>GCI;S*I7oF>x@NI#2(1gUMn0vE$4KIay3RN}Z<*USby6KK~+f
GjvN3MA6{?(

delta 73
zcmeBw#NYjZzoCV33zMMI_8ujs6lS5c{G#&2q7;RajKmU!{5*w{{Pu6EOxwSyG7B%7
e-gc8oeR@Cx6YKU<Czua%GNpTN@4dv_BnJTZ0~^Z#

diff --git a/sveltejs/src/QueryManager.js b/sveltejs/src/QueryManager.js
index 339fb6253..14970b398 100644
--- a/sveltejs/src/QueryManager.js
+++ b/sveltejs/src/QueryManager.js
@@ -22,6 +22,10 @@ function updateProjectsInCache (projects) {
   });
 }
 
+Drupal.AjaxCommands.prototype.refresh_project = (ajax, response) => {
+  updateProjectsInCache([response.project]);
+};
+
 /**
  * Handles fetching and temporarily caching project data from the backend.
  *
-- 
GitLab


From 4be07facfdab4db1824db3e5ac5cadc923dfbbdb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 08:19:44 -0500
Subject: [PATCH 13/23] I think it's working, ensure the activator does the
 right thing

---
 src/Controller/InstallerController.php        |  10 ++++------
 ...Command.php => RefreshProjectsCommand.php} |  10 +++++-----
 sveltejs/public/build/bundle.js               | Bin 278887 -> 279163 bytes
 sveltejs/public/build/bundle.js.map           | Bin 258319 -> 258595 bytes
 sveltejs/src/QueryManager.js                  |  10 +++++++---
 .../src/TestActivator.php                     |  13 +++++++++----
 .../ProjectBrowserInstallerUiTest.php         |  12 +++++++-----
 7 files changed, 32 insertions(+), 23 deletions(-)
 rename src/{RefreshProjectCommand.php => RefreshProjectsCommand.php} (50%)

diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php
index 0bd3f6b92..95fed907c 100644
--- a/src/Controller/InstallerController.php
+++ b/src/Controller/InstallerController.php
@@ -17,7 +17,7 @@ use Drupal\project_browser\ComposerInstaller\Installer;
 use Drupal\project_browser\EnabledSourceHandler;
 use Drupal\project_browser\InstallState;
 use Drupal\project_browser\ProjectBrowser\Normalizer;
-use Drupal\project_browser\RefreshProjectCommand;
+use Drupal\project_browser\RefreshProjectsCommand;
 use Drupal\system\SystemManager;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -436,6 +436,7 @@ final class InstallerController extends ControllerBase {
    */
   public function activate(Request $request): AjaxResponse {
     $response = new AjaxResponse();
+    $refreshed_projects = [];
 
     $projects = $request->getPayload()->get('projects') ?? [];
     if ($projects) {
@@ -455,10 +456,7 @@ final class InstallerController extends ControllerBase {
           $response->addCommand($command);
         }
         $this->installState->setState($project_id, 'installed');
-
-        $project = $this->normalizer->normalize($project, context: ['source' => $source_id]);
-        assert(is_array($project));
-        $response->addCommand(new RefreshProjectCommand($project));
+        $refreshed_projects[] = $this->normalizer->normalize($project, context: ['source' => $source_id]);
       }
       catch (\Throwable $e) {
         $message = $e->getMessage();
@@ -472,7 +470,7 @@ final class InstallerController extends ControllerBase {
       }
     }
     $this->installState->deleteAll();
-    return $response;
+    return $response->addCommand(new RefreshProjectsCommand($refreshed_projects));
   }
 
   /**
diff --git a/src/RefreshProjectCommand.php b/src/RefreshProjectsCommand.php
similarity index 50%
rename from src/RefreshProjectCommand.php
rename to src/RefreshProjectsCommand.php
index c6fab42a0..d3e99d39e 100644
--- a/src/RefreshProjectCommand.php
+++ b/src/RefreshProjectsCommand.php
@@ -7,12 +7,12 @@ namespace Drupal\project_browser;
 use Drupal\Core\Ajax\CommandInterface;
 
 /**
- * An AJAX command to refresh a particular project in the Svelte app.
+ * An AJAX command to refresh projects in the Svelte app.
  */
-final class RefreshProjectCommand implements CommandInterface {
+final class RefreshProjectsCommand implements CommandInterface {
 
   public function __construct(
-    private readonly array $project,
+    private readonly array $projects,
   ) {}
 
   /**
@@ -20,8 +20,8 @@ final class RefreshProjectCommand implements CommandInterface {
    */
   public function render(): array {
     return [
-      'command' => 'refresh_project',
-      'project' => $this->project,
+      'command' => 'refresh_projects',
+      'projects' => $this->projects,
     ];
   }
 
diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index 56b511bb69fe5939aa8f175e34aa259748724427..689ecb40102ab74270da73bcf8e0a2b6a2286bde 100644
GIT binary patch
delta 348
zcmaFfB>1~Uu%U&qg=q_O{rP%*eTC5CRD}e+(t?!4l2i@N1cl;!g_4ZK5{1N^9EIZ2
zq~heF%%s$!Vg-#%y;MCNh0MH^%(Bds(!?Bvz|z#BO5eo1#Prl6E(I{q*H_5QD=taQ
zOHM7;R4C8P$x%p3RmjUP$xO>kO;Je9OHn9FO-{`$OI0YzNL3KYNi0b%E>S2b%Fjwo
zE>TEHEJ@UxUU8mzvw^<8f@4ljez`(&Vsb`m3QS3{LP<WzNQiq=6v{Fa6&$@BBlM=L
zU0^m6FIKQs&`8Wmtk6-YMrf&>o^^p)nFDG|@%H%_m=7^~DI_N4mzF3b=2b!+ub`l>
zuaKOPn3tXkwg%)vm>z}lRE3PhvQ&lK{NfU@AM#3aDiw0_6H`)CwkKX;X3GTt|FU_5

delta 84
zcmey}BKW*Xu%U&qg=q_O{rTyV7ns*f|8;>`S4hEDK_f9Mu|h|oD7CmCKd(4-y4^))
jWua&oSFfNbKPxr4BzAIjztr~Ti_9ySx0haJX3GTto7^FS

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index d96ebd864fe3e5eac3aec7f841509642f6ffd99a..e5853269fa1e62e3bd5ea1ac95703df28c7bd45c 100644
GIT binary patch
delta 487
zcmeBw#J~6ne?tr77N$zYdVPI`(Bf2u1ijLNl*E!$4b23F;(UdYjKmU!#GD+3;?ktz
z<f6=^)S_YqjZD2%JspM2yp+tc%#_l^9EHHr)S^n?#Jt4x)S{R?1qFS5h0MI-lEl2^
z)M8DA^30qZg``x4y!?{Pw9M2Lg~YrRg`(8t)XcI}g_4X^g`C6^!PMdsg@U5|tkmQZ
zg_OjSM7`;yicE71^z{`Sb8_;_6_OK^Gg4DvN{SUq@<B#I9Gjw0mYJyF=;aupH=SRJ
zNngBJ!B#;dF)Oh`N1+;_rFMFN5|aW4)RyAyO-f8H%<c+_N%^HE3W<4@AeV#Po}7`G
zm!1l?1LQoI28Hreg^a|qRE6C9;u5e2@=9_l6>{<uQ&Lmf+0>Y}v#Bx5E#$ZMbaZwE
z6FRPrj;_-g`I$whUub7yo$h>``4|^O#@Er&clv{LCMo-LCr8J0CmnZ3M|T}(M@MHJ
zPe(^juucdO=;#;-VR<_`dV_RVxkC7bPL7U+(*qirB$;xYr{~^ea+rSV1oQdrQ%*8}
O=U^<~-gAk$Sq=bJ0-;X;

delta 189
zcmZ4dgunk0e?tr77N$zY>AXrzGp1ixV$u~-uvO4V%u1}#Q7B3+F38U-PMt2T%%mU`
z4ddz+6y;~7CYQu+cUER<VQ%MBXWGuE&Mdc(RmauQ(RKR9Tqe=!0VkMGPggj}EYFha
z=$Ja)Y8{i5n75;&x3#CEbAc;Zp-!Qbqhq1dbb&}F(dk{QnYgB3$Yo-gwt|^u`u*ul
ktkZkfGO<tpa+}F|`puKf=eMst#r%nb(PaCA%goJk06ro?DgXcg

diff --git a/sveltejs/src/QueryManager.js b/sveltejs/src/QueryManager.js
index 14970b398..5267398b5 100644
--- a/sveltejs/src/QueryManager.js
+++ b/sveltejs/src/QueryManager.js
@@ -14,6 +14,8 @@ function getFromCache (projects) {
 }
 
 function updateProjectsInCache (projects) {
+  // Use `.update()` so that all subscribers (i.e., individual QueryManager
+  // instances) will be notified and receive the latest project data.
   cache.update((cacheData) => {
     projects.forEach((project) => {
       cacheData[project.id] = project;
@@ -22,8 +24,9 @@ function updateProjectsInCache (projects) {
   });
 }
 
-Drupal.AjaxCommands.prototype.refresh_project = (ajax, response) => {
-  updateProjectsInCache([response.project]);
+// Allow cached projects to be updated via AJAX.
+Drupal.AjaxCommands.prototype.refresh_projects = (ajax, { projects }) => {
+  updateProjectsInCache(projects);
 };
 
 /**
@@ -47,7 +50,8 @@ export default class {
     // recent query.
     this.count = 0;
 
-    // Whenever the cache changes, we want to notify our subscribers.
+    // Whenever the cache changes, we want to notify our subscribers about any
+    // changes to the projects we have most recently loaded.
     cache.subscribe(() => {
       const projects = getFromCache(this.list);
 
diff --git a/tests/modules/project_browser_test/src/TestActivator.php b/tests/modules/project_browser_test/src/TestActivator.php
index f10a5e9cd..8a8bb74d3 100644
--- a/tests/modules/project_browser_test/src/TestActivator.php
+++ b/tests/modules/project_browser_test/src/TestActivator.php
@@ -40,7 +40,12 @@ class TestActivator implements ActivatorInterface {
    * {@inheritdoc}
    */
   public function getStatus(Project $project): ActivationStatus {
-    if ($project->machineName === 'pinky_brain') {
+    $activated_projects = $this->state->get("test activator", []);
+
+    if (in_array($project->id, $activated_projects, TRUE)) {
+      return ActivationStatus::Active;
+    }
+    elseif ($project->machineName === 'pinky_brain') {
       return ActivationStatus::Present;
     }
     return ActivationStatus::Absent;
@@ -50,9 +55,9 @@ class TestActivator implements ActivatorInterface {
    * {@inheritdoc}
    */
   public function activate(Project $project): null {
-    $log_message = $this->state->get("test activator", []);
-    $log_message[] = "$project->title was activated!";
-    $this->state->set("test activator", $log_message);
+    $activated_projects = $this->state->get("test activator", []);
+    $activated_projects[] = $project->id;
+    $this->state->set("test activator", $activated_projects);
     return NULL;
   }
 
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
index c29e71767..d2d474858 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
@@ -80,9 +80,10 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     $this->drupalGet('admin/modules/browse/project_browser_test_mock');
     $this->installProject('Cream cheese on a bagel');
 
-    // The activator in project_browser_test should have logged a message.
+    // The activator in project_browser_test should have logged the unqualified
+    // project ID.
     // @see \Drupal\project_browser_test\TestActivator
-    $this->assertContains('Cream cheese on a bagel was activated!', $this->container->get(StateInterface::class)->get('test activator'));
+    $this->assertContains('cream_cheese', $this->container->get(StateInterface::class)->get('test activator'));
   }
 
   /**
@@ -281,12 +282,13 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
     $this->waitForProjectToBeInstalled($cream_cheese);
     $this->waitForProjectToBeInstalled($kangaroo);
 
-    // The activator in project_browser_test should have logged a message.
+    // The activator in project_browser_test should have logged the unqualified
+    // project IDs.
     // @see \Drupal\project_browser_test\TestActivator
     $activated = $this->container->get(StateInterface::class)
       ->get('test activator');
-    $this->assertContains('Cream cheese on a bagel was activated!', $activated);
-    $this->assertContains('Kangaroo was activated!', $activated);
+    $this->assertContains('cream_cheese', $activated);
+    $this->assertContains('kangaroo', $activated);
   }
 
   /**
-- 
GitLab


From 0ffb1253c70d2312f1ca972c1d59304dff2e0a35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 08:30:40 -0500
Subject: [PATCH 14/23] Fix a few failing tests

---
 .../FunctionalJavascript/ProjectBrowserInstallerUiTest.php  | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
index d2d474858..f5afc262a 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php
@@ -177,6 +177,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
    * @covers ::unlock
    */
   public function testCanBreakStageWithMissingProjectBrowserLock(): void {
+    TestActivator::handle('drupal/cream_cheese');
+
     // Start install begin.
     $this->drupalGet('admin/modules/project_browser/install-begin', [
       'query' => ['source' => 'project_browser_test_mock'],
@@ -204,6 +206,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
    * @covers ::unlock
    */
   public function testCanBreakLock(): void {
+    TestActivator::handle('drupal/cream_cheese');
+
     // Start install begin.
     $this->drupalGet('admin/modules/project_browser/install-begin', [
       'query' => ['source' => 'project_browser_test_mock'],
@@ -239,6 +243,8 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase {
    * Confirm that a status check warning allows download and install.
    */
   public function testPackageManagerWarningAllowsDownloadInstall(): void {
+    TestActivator::handle('drupal/cream_cheese');
+
     // @see \Drupal\project_browser_test\TestInstallReadiness
     $this->container->get(StateInterface::class)
       ->set('project_browser_test.simulated_result_severity', SystemManager::REQUIREMENT_WARNING);
-- 
GitLab


From 496916f28755dafe9a8b6bafae20d186c93fb6cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 08:50:35 -0500
Subject: [PATCH 15/23] Fix one broken test by resetting state cache in
 RecipeActivator

---
 src/Activator/RecipeActivator.php | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/Activator/RecipeActivator.php b/src/Activator/RecipeActivator.php
index 2cff5fc00..bee718379 100644
--- a/src/Activator/RecipeActivator.php
+++ b/src/Activator/RecipeActivator.php
@@ -68,6 +68,8 @@ final class RecipeActivator implements InstructionsInterface, EventSubscriberInt
    */
   public function getStatus(Project $project): ActivationStatus {
     $path = $this->getPath($project);
+    // Ensure we're getting the most up-to-date information.
+    $this->state->resetCache();
 
     if (in_array($path, $this->state->get(static::STATE_KEY, []), TRUE)) {
       return ActivationStatus::Active;
-- 
GitLab


From 4a488d921d22e1d4ef67eb7e25669f2dc320e7a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 09:01:45 -0500
Subject: [PATCH 16/23] Tiny cleanup

---
 src/Controller/InstallerController.php | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/Controller/InstallerController.php b/src/Controller/InstallerController.php
index 95fed907c..161f4628a 100644
--- a/src/Controller/InstallerController.php
+++ b/src/Controller/InstallerController.php
@@ -436,7 +436,7 @@ final class InstallerController extends ControllerBase {
    */
   public function activate(Request $request): AjaxResponse {
     $response = new AjaxResponse();
-    $refreshed_projects = [];
+    $activated_projects = [];
 
     $projects = $request->getPayload()->get('projects') ?? [];
     if ($projects) {
@@ -446,6 +446,7 @@ final class InstallerController extends ControllerBase {
     assert(is_array($projects));
     foreach ($projects as $project_id) {
       $this->installState->setState($project_id, 'activating');
+      // $project_id is fully qualified and has the form `SOURCE_ID/LOCAL_ID`.
       [$source_id] = explode('/', $project_id, 2);
 
       try {
@@ -456,7 +457,7 @@ final class InstallerController extends ControllerBase {
           $response->addCommand($command);
         }
         $this->installState->setState($project_id, 'installed');
-        $refreshed_projects[] = $this->normalizer->normalize($project, context: ['source' => $source_id]);
+        $activated_projects[] = $this->normalizer->normalize($project, context: ['source' => $source_id]);
       }
       catch (\Throwable $e) {
         $message = $e->getMessage();
@@ -470,7 +471,7 @@ final class InstallerController extends ControllerBase {
       }
     }
     $this->installState->deleteAll();
-    return $response->addCommand(new RefreshProjectsCommand($refreshed_projects));
+    return $response->addCommand(new RefreshProjectsCommand($activated_projects));
   }
 
   /**
-- 
GitLab


From 8ea2fd402ef0570b215ee6d4d4d0a63d44859391 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 11:40:03 -0500
Subject: [PATCH 17/23] Use project ID as index to hopefully prevent flickering

---
 sveltejs/public/build/bundle.js     | Bin 279163 -> 279138 bytes
 sveltejs/public/build/bundle.js.map | Bin 258595 -> 258613 bytes
 sveltejs/src/ProjectBrowser.svelte  |   2 +-
 3 files changed, 1 insertion(+), 1 deletion(-)

diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index 689ecb40102ab74270da73bcf8e0a2b6a2286bde..cecd5d94d1a117891778ff9a5cfcbc6634c4108f 100644
GIT binary patch
delta 55
zcmey}BKW99u%U&qg=q`(ru)+cJ~Ap!e|?`>l0!kGD8F1UGiAHL1LnP~%zBwA(;2@o
M+HdE1#+)Am0C=?(VgLXD

delta 77
zcmaFVBKW&Su%U&qg=q`(ru);olbN~MEMgUG6*4DZ_^2`c@_lAWJ{^V3yp+@m1&yNo
ja?RSwAJ>RYH>hFc+%Ecnc@FDz#xIQa+dn^L&W`~As4^au

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index e5853269fa1e62e3bd5ea1ac95703df28c7bd45c..a012e1af66e538537ad02c48022de757983f9f89 100644
GIT binary patch
delta 103
zcmZ4dgn#Q3{)QIDEld}EI21IB^2_xyQ`+zPFm1o<!@RF;`kc$m?Pi{ij=?(4j*gBx
nz97OMM7YC=NGQu)$J^1-S;y7U(G{$Cx}6@g-S&nn%!_3KgE1k?

delta 75
zcmdn`gn#i9{)QIDEld}Ecytsp^HNeP6f}zR%iC}IFm1o-!@RF;`n1c;9o+fOj*jV0
eI_{2+?$ZM;nZ;N^0;ba)^_b1KPrAaqSQY?Xv>R^#

diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte
index 3a9ed182e..d126cd6d1 100644
--- a/sveltejs/src/ProjectBrowser.svelte
+++ b/sveltejs/src/ProjectBrowser.svelte
@@ -248,7 +248,7 @@
       </div>
     </div>
     {#key $updated}
-      {#each rows as row, index (row)}
+      {#each rows as row (row.id)}
         <Project {toggleView} project={row} />
       {/each}
     {/key}
-- 
GitLab


From d8bcbfaab212bfb97c737129e01bb1a3fc9b8864 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 11:52:20 -0500
Subject: [PATCH 18/23] Add low-level test coverage that multiple modules can
 be activated at once

---
 .../Functional/InstallerControllerTest.php    | 32 +++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php
index 68480eb72..872efb976 100644
--- a/tests/src/Functional/InstallerControllerTest.php
+++ b/tests/src/Functional/InstallerControllerTest.php
@@ -648,4 +648,36 @@ class InstallerControllerTest extends BrowserTestBase {
     $this->assertSession()->addressEquals('/admin/modules/browse/drupal_core');
   }
 
+  /**
+   * Tests activating multiple core modules in a single request.
+   */
+  public function testActivateMultipleModules(): void {
+    $this->config('project_browser.admin_settings')
+      ->set('enabled_sources', ['drupal_core'])
+      ->save();
+
+    // Prime the project cache.
+    $this->container->get(EnabledSourceHandler::class)
+      ->getProjects('drupal_core');
+
+    $response = $this->makeApiRequest(
+      'POST',
+      Url::fromRoute('project_browser.activate'),
+      [
+        RequestOptions::BODY => Json::encode([
+          'projects' => implode(',', [
+            'drupal_core/language',
+            'drupal_core/content_translation',
+            'drupal_core/locale',
+          ]),
+        ]),
+      ],
+    );
+    $this->assertSame(200, $response->getStatusCode());
+    $commands = Json::decode($response->getBody()->getContents());
+    $this->assertSame('refresh_projects', $commands[0]['command']);
+    $statuses = array_column($commands[0]['projects'], 'status');
+    $this->assertSame(['active'], array_unique($statuses));
+  }
+
 }
-- 
GitLab


From a6ca113e8d435be965211ecadf35f6a08fcda1fb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 12:18:31 -0500
Subject: [PATCH 19/23] Fix the bug by restoring the module info from the
 global service wrapper

---
 src/Activator/ModuleActivator.php | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/src/Activator/ModuleActivator.php b/src/Activator/ModuleActivator.php
index 6edee6617..b6731d8d4 100644
--- a/src/Activator/ModuleActivator.php
+++ b/src/Activator/ModuleActivator.php
@@ -25,8 +25,8 @@ final class ModuleActivator implements InstructionsInterface, TasksInterface {
 
   public function __construct(
     private readonly ModuleInstallerInterface $moduleInstaller,
-    private readonly ModuleExtensionList $moduleList,
-    private readonly ModuleHandlerInterface $moduleHandler,
+    private ModuleExtensionList $moduleList,
+    private ModuleHandlerInterface $moduleHandler,
     private readonly FileUrlGeneratorInterface $fileUrlGenerator,
     private readonly RequestStack $requestStack,
     private readonly AccountInterface $currentUser,
@@ -36,7 +36,7 @@ final class ModuleActivator implements InstructionsInterface, TasksInterface {
    * {@inheritdoc}
    */
   public function getStatus(Project $project): ActivationStatus {
-    if (array_key_exists($project->machineName, $this->moduleList->getAllInstalledInfo())) {
+    if ($this->moduleHandler->moduleExists($project->machineName)) {
       return ActivationStatus::Active;
     }
     elseif (array_key_exists($project->machineName, $this->moduleList->getAllAvailableInfo())) {
@@ -57,6 +57,14 @@ final class ModuleActivator implements InstructionsInterface, TasksInterface {
    */
   public function activate(Project $project): null {
     $this->moduleInstaller->install([$project->machineName]);
+
+    // The container has changed, so we need to reload the module handler and
+    // module list from the global service wrapper.
+    // @phpstan-ignore-next-line
+    $this->moduleHandler = \Drupal::moduleHandler();
+    // @phpstan-ignore-next-line
+    $this->moduleList = \Drupal::service(ModuleExtensionList::class);
+
     return NULL;
   }
 
-- 
GitLab


From 2efe8cf8cf5d8f3d2f79f014a4f9597be27ad6f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 12:57:30 -0500
Subject: [PATCH 20/23] Fix ModuleActivatorTest

---
 tests/src/Functional/InstallerControllerTest.php |  4 +++-
 tests/src/Kernel/ModuleActivatorTest.php         | 14 ++++++++------
 2 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php
index 872efb976..bd24d048f 100644
--- a/tests/src/Functional/InstallerControllerTest.php
+++ b/tests/src/Functional/InstallerControllerTest.php
@@ -674,7 +674,9 @@ class InstallerControllerTest extends BrowserTestBase {
       ],
     );
     $this->assertSame(200, $response->getStatusCode());
-    $commands = Json::decode($response->getBody()->getContents());
+    $body = $response->getBody()->getContents();
+    $commands = Json::decode($body);
+    $this->assertNotEmpty($commands, $body);
     $this->assertSame('refresh_projects', $commands[0]['command']);
     $statuses = array_column($commands[0]['projects'], 'status');
     $this->assertSame(['active'], array_unique($statuses));
diff --git a/tests/src/Kernel/ModuleActivatorTest.php b/tests/src/Kernel/ModuleActivatorTest.php
index 1977ea6ad..ceac2d066 100644
--- a/tests/src/Kernel/ModuleActivatorTest.php
+++ b/tests/src/Kernel/ModuleActivatorTest.php
@@ -26,6 +26,7 @@ class ModuleActivatorTest extends KernelTestBase {
   protected static $modules = [
     'block',
     'breakpoint',
+    'help',
     'project_browser',
     'user',
   ];
@@ -49,6 +50,13 @@ class ModuleActivatorTest extends KernelTestBase {
    */
   protected function setUp(): void {
     $this->mockModuleHandler = $this->createMock(ModuleHandlerInterface::class);
+    $this->mockModuleHandler->expects($this->atLeastOnce())
+      ->method('moduleExists')
+      ->willReturnMap(array_map(
+        fn (string $module_name): array => [$module_name, TRUE],
+        static::$modules,
+      ));
+
     parent::setUp();
 
     $this->config('project_browser.admin_settings')
@@ -107,12 +115,6 @@ class ModuleActivatorTest extends KernelTestBase {
    *   [false]
    */
   public function testHelpLinksAreExposed(bool $implements_hook_help): void {
-    $this->enableModules(['help']);
-
-    $this->mockModuleHandler->expects($this->atLeastOnce())
-      ->method('moduleExists')
-      ->with('help')
-      ->willReturn(TRUE);
     $this->mockModuleHandler->expects($this->atLeastOnce())
       ->method('hasImplementations')
       ->with('help', 'breakpoint')
-- 
GitLab


From 101ba9ce43484875b3a37b5c4d2a590c78411e2d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ph=C3=A9na=20Proxima?= <adam@phenaproxima.net>
Date: Wed, 26 Feb 2025 13:13:42 -0500
Subject: [PATCH 21/23] Take another approach in InstallerControllerTest

---
 .../Functional/InstallerControllerTest.php    | 20 ++++++-------------
 1 file changed, 6 insertions(+), 14 deletions(-)

diff --git a/tests/src/Functional/InstallerControllerTest.php b/tests/src/Functional/InstallerControllerTest.php
index bd24d048f..55769e6d4 100644
--- a/tests/src/Functional/InstallerControllerTest.php
+++ b/tests/src/Functional/InstallerControllerTest.php
@@ -660,26 +660,18 @@ class InstallerControllerTest extends BrowserTestBase {
     $this->container->get(EnabledSourceHandler::class)
       ->getProjects('drupal_core');
 
-    $response = $this->makeApiRequest(
-      'POST',
+    $response = $this->getPostResponse(
       Url::fromRoute('project_browser.activate'),
       [
-        RequestOptions::BODY => Json::encode([
-          'projects' => implode(',', [
-            'drupal_core/language',
-            'drupal_core/content_translation',
-            'drupal_core/locale',
-          ]),
+        'projects' => implode(',', [
+          'drupal_core/language',
+          'drupal_core/content_translation',
+          'drupal_core/locale',
         ]),
       ],
     );
     $this->assertSame(200, $response->getStatusCode());
-    $body = $response->getBody()->getContents();
-    $commands = Json::decode($body);
-    $this->assertNotEmpty($commands, $body);
-    $this->assertSame('refresh_projects', $commands[0]['command']);
-    $statuses = array_column($commands[0]['projects'], 'status');
-    $this->assertSame(['active'], array_unique($statuses));
+    $this->assertSame(3, substr_count((string) $response->getBody(), '"status":"active"'));
   }
 
 }
-- 
GitLab


From b8f1136af70426458b91d41baa81be91db1f7690 Mon Sep 17 00:00:00 2001
From: Tim Plunkett <git@plnktt.com>
Date: Wed, 26 Feb 2025 13:25:53 -0500
Subject: [PATCH 22/23] Use targeted ignores for PHPStan

---
 src/Activator/ModuleActivator.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Activator/ModuleActivator.php b/src/Activator/ModuleActivator.php
index b6731d8d4..527216b50 100644
--- a/src/Activator/ModuleActivator.php
+++ b/src/Activator/ModuleActivator.php
@@ -60,9 +60,9 @@ final class ModuleActivator implements InstructionsInterface, TasksInterface {
 
     // The container has changed, so we need to reload the module handler and
     // module list from the global service wrapper.
-    // @phpstan-ignore-next-line
+    // @phpstan-ignore globalDrupalDependencyInjection.useDependencyInjection
     $this->moduleHandler = \Drupal::moduleHandler();
-    // @phpstan-ignore-next-line
+    // @phpstan-ignore globalDrupalDependencyInjection.useDependencyInjection
     $this->moduleList = \Drupal::service(ModuleExtensionList::class);
 
     return NULL;
-- 
GitLab


From ce140dd96ed4b0e98595f99e91100517a67fa3bd Mon Sep 17 00:00:00 2001
From: Tim Plunkett <git@plnktt.com>
Date: Wed, 26 Feb 2025 14:35:07 -0500
Subject: [PATCH 23/23] Revert "Use targeted ignores for PHPStan"

This reverts commit b8f1136af70426458b91d41baa81be91db1f7690.
---
 src/Activator/ModuleActivator.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/Activator/ModuleActivator.php b/src/Activator/ModuleActivator.php
index 527216b50..b6731d8d4 100644
--- a/src/Activator/ModuleActivator.php
+++ b/src/Activator/ModuleActivator.php
@@ -60,9 +60,9 @@ final class ModuleActivator implements InstructionsInterface, TasksInterface {
 
     // The container has changed, so we need to reload the module handler and
     // module list from the global service wrapper.
-    // @phpstan-ignore globalDrupalDependencyInjection.useDependencyInjection
+    // @phpstan-ignore-next-line
     $this->moduleHandler = \Drupal::moduleHandler();
-    // @phpstan-ignore globalDrupalDependencyInjection.useDependencyInjection
+    // @phpstan-ignore-next-line
     $this->moduleList = \Drupal::service(ModuleExtensionList::class);
 
     return NULL;
-- 
GitLab