From aaf207c4bbdc4bd91124d58c700dd0fca9bd91f2 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Mon, 24 Aug 2026 11:31:36 -0400 Subject: [PATCH 1/4] refactor(sync): read links from CouchDB view in both sync paths --- backend/sync/incrementalSync.js | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/backend/sync/incrementalSync.js b/backend/sync/incrementalSync.js index 766f1a5..d321430 100644 --- a/backend/sync/incrementalSync.js +++ b/backend/sync/incrementalSync.js @@ -416,9 +416,10 @@ async function firstSync(dbname) { const linkRows = await fetchView(dbname, "links"); let linkCount = 0; for (const row of linkRows) { - const fileType = row.key?.[0]; + // links view key is now [doc._id, ext, size] + const fileType = row.key?.[1]; if (!isValidFileType(fileType)) continue; - const subjId = String(row.key?.[1] || ""); + const subjId = String(row.key?.[2] || ""); await insertIolink(dbname, row.id, subjId, fileType, { key: row.key, value: row.value, @@ -431,13 +432,26 @@ async function firstSync(dbname) { // === Process one changed dataset (Option A: 2 HTTP requests + local transforms) === async function processDatasetUpdate(dbname, dsname) { - // dbinfo view supports key filtering; raw doc carries everything else. + // dbinfo view supports key filtering; raw doc carries subjects; links view + // is now filterable by dataset id (key = [doc._id, ext, size]) via a range + // query, so links come straight from the view — same source as firstSync. const keyParam = encodeURIComponent(JSON.stringify(dsname)); - const [dbinfoRes, rawDocRes] = await Promise.all([ + const linkStart = encodeURIComponent(JSON.stringify([dsname])); + const linkEnd = encodeURIComponent(JSON.stringify([dsname, {}])); + const [dbinfoRes, rawDocRes, linkRes] = await Promise.all([ axios.get( `${COUCHDB_URL}/${dbname}/_design/qq/_view/dbinfo?key=${keyParam}` ), axios.get(`${COUCHDB_URL}/${dbname}/${encodeURIComponent(dsname)}`), + axios + .get( + `${COUCHDB_URL}/${dbname}/_design/qq/_view/links?startkey=${linkStart}&endkey=${linkEnd}` + ) + .catch((err) => { + // DBs without a links view (404) → treat as no links. + if (err.response?.status === 404) return { data: { rows: [] } }; + throw err; + }), ]); const dbinfoRow = (dbinfoRes.data.rows || [])[0]; @@ -449,7 +463,7 @@ async function processDatasetUpdate(dbname, dsname) { const doc = rawDocRes.data; const subjectRows = transformSubjects(doc); - const linkRows = transformLinks(doc); + const linkRows = linkRes.data.rows || []; // Rule 1: wrap all writes for this dataset in one transaction. await sequelize.transaction(async (t) => { @@ -493,8 +507,10 @@ async function processDatasetUpdate(dbname, dsname) { { replacements: { dbname, dsname }, transaction: t } ); for (const row of linkRows) { - const fileType = row.key?.[0]; - const subjId = String(row.key?.[1] || ""); + // links view key is [doc._id, ext, size] + const fileType = row.key?.[1]; + if (!isValidFileType(fileType)) continue; + const subjId = String(row.key?.[2] || ""); await insertIolink( dbname, dsname, From 15beb5e424df2eb31af1ce9cc3399e7c71fe02de Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Mon, 24 Aug 2026 11:54:07 -0400 Subject: [PATCH 2/4] feat(sync): add refreshLinks.js to rebuild iolinks from id-first links view; read card file size from key[2] --- backend/sync/refreshLinks.js | Bin 0 -> 4030 bytes src/components/SearchPage/DatasetCard.tsx | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 backend/sync/refreshLinks.js diff --git a/backend/sync/refreshLinks.js b/backend/sync/refreshLinks.js new file mode 100644 index 0000000000000000000000000000000000000000..0ac75e371320aef1fcb5042c0ce425f0d333d3c5 GIT binary patch literal 4030 zcmb7H>rUfH7T(`H#Tkp8b=1M8TeG`TdeYJWX$>P9R$O|10D_8LNeWDryQ&OiGm+*Y z<_Y&n_B&N}64>rtZGM1VmvjBjcaD?BNpjw*!YA*yw(^`#wA^ts6+%sCo+j2jgzH6P zt<;|OO;gMc_9)1rM>J7uN?VRprh^S?B3q&grr=MRudg zKFMbfmFl1Jy3c|N^H7^cmpfRO+9{*q$W%V}snKepWJ!;rYT)He)Urp;K!9?d6}CP` z7e#a{2Emgzh>G@uToM9WhebhabzahVBn^xq8a7>^ffSoEg^E zluZoew?&y+BHWENe0lUN9_mW|PyOSG24Km-)3JGe3wJl9u%P3U^U(kiP>xCi+#HOK zM(3j)RGi*9JU$zpo>PsI=<*UGD(GPEFDOBdXDp8vs$;9D_7~)^BP*0S zEjQ0)S^gCO^Tk&3{>jDu&j))qTp2?`t_$fLgE!mU`RzAKX5QCskmn)Ich2ZcnZe)j zYsvB-#`!7mMZ7nLsdvC(tYFPF0OGQu0EN8|BwHgAbNUlWSAc1N?Mb5}%AA7L_VL zBJbz(TBbdwlCKfS%97VNz?m;>NnsX|hcF}@n=~8_DT&4<^!@i?8|$!FSnqxXw$da^ zdfnD{yax-arj#yC^7RgHR8mg?#vh0DZWofGJAx}2yxPX9E$M@{yQHzAqmc|9Q^~9{ z)AaUAvu)dpcwjl8i3D9UpT;6#LT2-Cp@yiEs0(y5e)e`A;!jL?z#^x3Na9JLb^;8u zsq`@tM7P_E4csjAmBRc6E~=@{zkJDWSXmzQ-u6R`h!*74gGARD(BC+PyL~j2=Y7L% z*B^yQ4CoLvt(BjLy**RTUvJPdhzmXdG2Dx}Xk5k=k%f0a`SP0mT2c`@Rw}KH0V7fq z;#UC5)o@lKQMCEaiS!MGB9=ew{?J{Ep;^V6Hsh?#W`t9|1)jQBb&cT1Xfuc+y4fyR z7XJDf>}Bo2SS-EBCx!7KxHy11cC7`>l7-?pIBFmL)<*seq_}G3A@+UdId1n^JEUE9o7Y0D)=NBFr7aQ>9~2(@-em$#Z{V3HA+Yn;h9L;=0L`s}34x&k zKIThEd;56#Hh?8U%Dtol)8* zua7LYR|fIUfPsN!X87K^bi4b2QsCh(%d2&2^W7CYI}$}>j7G>Yr*52OU4vld;fr-v z4&G|BwRwZ>!HpMvW5IK96psh^%VTYPj}z{)w-ym_$X31Jj}DDpE}0#=HTpCCm1!za z!EWz$aS~yY+Fo?0mn&q)ibr31Sx2}RqxxxFim#7AEV4;<|>7L`mUED+-TY9o&mcF9YDxoi1pi1 zP8S3gyY>szWo*|Nk}$Lm{{Lz8N+ = ({ return m ? m[0] : ""; }; - // File size stored in key[1] of each iolinks row (bytes). Format for humans. + // File size stored in key[2] of each iolinks row (bytes; key = [id, ext, size]). Format for humans. const formatBytes = (n?: number): string => { if (typeof n !== "number" || !Number.isFinite(n) || n < 0) return ""; if (n < 1024) return `${n} B`; @@ -525,8 +525,8 @@ const DatasetCard: React.FC = ({ const v = f.value || {}; const subjTag = subjectFromPath(v.path); const sizeBytes = - Array.isArray(f.key) && typeof f.key[1] === "number" - ? f.key[1] + Array.isArray(f.key) && typeof f.key[2] === "number" + ? f.key[2] : undefined; const sizeTag = formatBytes(sizeBytes); const meta = [subjTag, sizeTag].filter(Boolean).join(" · "); From 2f871a1e696299aad72c353148b44dd0a3977280 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Mon, 24 Aug 2026 13:20:22 -0400 Subject: [PATCH 3/4] refactor(sync): rebuild links via per-dataset view fetch; drop transformLinks --- backend/sync/incrementalSync.js | 54 +++----------------------------- backend/sync/refreshLinks.js | Bin 4030 -> 5303 bytes 2 files changed, 4 insertions(+), 50 deletions(-) diff --git a/backend/sync/incrementalSync.js b/backend/sync/incrementalSync.js index d321430..f563905 100644 --- a/backend/sync/incrementalSync.js +++ b/backend/sync/incrementalSync.js @@ -227,56 +227,10 @@ function transformSubjects(doc) { return results; } -function transformLinks(doc) { - const results = []; - const filenameRe = /file=([^\/]*\/)*([^&\/\.]+)(\.[^.&%:]+(\.gz)*)([&:].*)*$/; - const filesizeRe = /size=(\d+)/; - const jsonpathRe = /:(\$[^&]+)/; - const urlhash = {}; - - function traverse(obj, level, rootpath) { - if (level > 10) return; - if (obj === null || typeof obj !== "object") return; - - for (const subkey of Object.keys(obj)) { - const v = obj[subkey]; - if ( - subkey === "_DataLink_" && - typeof v === "string" && - v.indexOf("http") !== -1 - ) { - const url = v; - const uniqurl = url.split(":$")[0]; - if (!Object.prototype.hasOwnProperty.call(urlhash, uniqurl)) { - const fname = url.match(filenameRe); - const fsize = url.match(filesizeRe); - let jpath = url.match(jsonpathRe); - if (jpath !== null && jpath.length) jpath = jpath[1]; - urlhash[uniqurl] = 1; - if (fname && fsize) { - results.push({ - id: doc._id, - key: [fname[3], parseInt(fsize[1], 10)], - value: { - path: rootpath, - url: uniqurl, - file: fname[2] + fname[3], - suffix: fname[3], - ref: jpath, - }, - }); - } - } - } - if (typeof v === "object" && v !== null) { - traverse(v, level + 1, rootpath + "." + subkey); - } - } - } - - traverse(doc, 1, "$"); - return results; -} +// transformLinks() removed: links now come straight from the CouchDB links +// view (id-first key [doc._id, ext, size]) in both firstSync and +// processDatasetUpdate, so there's a single source of truth and no regex +// drift between the two paths. // === DB helpers (each accepts an optional transaction) === diff --git a/backend/sync/refreshLinks.js b/backend/sync/refreshLinks.js index 0ac75e371320aef1fcb5042c0ce425f0d333d3c5..ebe3f5ad31520ed620fdff4726aedf07a62dcb2f 100644 GIT binary patch delta 1670 zcmb7E&u<$=6qX3o%{oX(y&x2khcRK7xNB1hB+@#N*ad>p7NxaN6`6SBd20_{&xV-I!7d*u$V)Wlw+@2 z1^-p0BdKYpeo(r^)K;mzj)E=xuS~LbKxtG-Qn^UDnxlT2;7w|QBZxGlqKoj`M_<^I z=`jj{LzzCWARZJIkhTZWZBSZr(F4UlK=40&>BS3Ic7o7+y7YV1 z;X6ioUZx}9LZQ?OHmDbZL^=VTh<1TRI#Q^EyC;uUADI$>H&8o_38#oOo(B*Q1Spjd zL378Ea~LJbAxpao{Dk-VdW819=>k)cj4Kv_NP$A&C}kW~5JF24%{t8)pPkKu%wbG5 zs1!yB0#69r(J+o3Z3vDrTX2C6V(g!E{V$@tNR368cotpFQvpx*P&aczcA4_ok%$Hu zz{E3ag{NQZlqZdAt;UuiLv(U|{dWTPS-!C09RJN=^eZ z8w#2+OE6AREy1$A}!pbjXyG~BKI-9|b%CMZJt zZ(YB3mGIj+=keG-$dlU^{5+dH_aAdtTxU{=Y#Eqyg$Pb9T%I@-+1$eA3DwPvF1-7g zlf(Q{XooXp4%!zXAMewFbi(Mq^&3iQ(PieVGgqb(kA^&9)~9)Jys+t^jIU&pllkzazyHa2o-wiT5l8s;lJ2H(;099*c?fOLXd?yQ`G1pxrU lFr*uRvh7Xa)Ohv*IVx5rck?34a_u|j&&OBqe!OyN=`TsYBtHNE delta 439 zcmdn4xlew>B<9J5EKfGKuK(6r`K zP{>IwQ7FnUFP?mcD{-3vs#B8k z5_3~)trYU|k|!9#T-y)6-L_ PhN-TFo3%MkFp?Pn74n6y From 2a5063fcde552dfe5cdbc59c25818e4a97b6e874 Mon Sep 17 00:00:00 2001 From: elainefan331 Date: Mon, 24 Aug 2026 15:53:05 -0400 Subject: [PATCH 4/4] perf(sync): batch iolinks inserts in refreshLinks (multi-row INSERT) --- backend/sync/refreshLinks.js | Bin 5303 -> 6098 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/backend/sync/refreshLinks.js b/backend/sync/refreshLinks.js index ebe3f5ad31520ed620fdff4726aedf07a62dcb2f..72f8ad72ef14ed6c5402a4825b0a121a77cbcdd5 100644 GIT binary patch delta 1084 zcmaJ=(M}UV6b<^eL8))PnA{MeyS6R`tq*MqL2M!sF##bagxKlsUAkpDv(D_YVlCw# z>W64}@IU+nAB>;jo!J(tCO*y1oVn+od-lvX@B8wX_4(2AGHeNrDg_1|mIhsr5%5@? zfWknkP(vQ4U3k6If_}!+_>q!1Z0+o|cHe+21;YCb)ftq5z8xY9WWOJ$5J?4T7a{LT zjs>v?vCt{9&~#u$U3oOQpP&N6{Nv)Yt)so^WEfeJD9K1ztl@m6TKxc;%be~@0Wv`$ zFjErT^A-QeE5o6^3ZPCfh#BW+z>b;e3(Rl{OmnGDFofsNd?%1%L`X5syUiE-J1?OL zwQ98ibeC332M?h}V{uK`syR%UVtrkK965(fge2u@t@gO)I82|30HRC;X-w)Op6Tv3 zd8yq_7DcE$7&riN1e3kbM<|ZT-e$82RgWxBGbI{yhoq6RQ^qsgffOd3 zjs`<>RIvMGdW(#5T~&de2hBCx4rXZlgfzKrw;nDHdc(>H>Y=t@P@k@+lYP&uUfVMr zfV3t2ux+c_N7PyQVMnk&&Y+fJ3hTe&>!{8;Jt{r8Vzyl;rKd6G*Lkgy3D(onIyAJp zSk5SI8YzW5{)oO!qw9>5g^kUFjqUx`9xM&6+vxXXEZoHkEPBIs#ZES`v#|jCC{kUw zI~Wq=H7*S3vZffB*mh delta 273 zcmcblzg=^KJJ|U#WX3eDl1e50rNKEGE zXPqp-?^q8K2y^rabq!X~uu4hFOUzBxQLsuWhB83ff&8+})N&w|Rh*xvnV8Yl*~Lc>a-D8F1UGX-HN$Y7AQR#3M?WF~(Q lQkcvm>?{tFu0^P>g&I0JKv;6}W#Q_{5@Mp8r-|4w0{}-WM(+Rs