diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts index 97d6d6830..e69c9812f 100644 --- a/app/features/user-page/UserRepository.server.ts +++ b/app/features/user-page/UserRepository.server.ts @@ -146,16 +146,23 @@ export function findLayoutDataByIdentifier( ) .whereRef("VideoMatchPlayer.playerUserId", "=", "User.id") .as("vodsCount"), + // authored and tagged art counted via an indexed union: an OR spanning + // Art and ArtUserMetadata would make SQLite scan the whole Art table eb .selectFrom("Art") - .leftJoin("ArtUserMetadata", "ArtUserMetadata.artId", "Art.id") .innerJoin("UserSubmittedImage", "UserSubmittedImage.id", "Art.imgId") - .select(({ fn }) => fn.count("Art.id").distinct().as("count")) - .where((innerEb) => - innerEb.or([ - innerEb("Art.authorId", "=", eb.ref("User.id")), - innerEb("ArtUserMetadata.userId", "=", eb.ref("User.id")), - ]), + .select(({ fn }) => fn.countAll().as("count")) + .where("Art.id", "in", (innerEb) => + innerEb + .selectFrom("Art") + .select("Art.id") + .whereRef("Art.authorId", "=", "User.id") + .union( + innerEb + .selectFrom("ArtUserMetadata") + .select("ArtUserMetadata.artId as id") + .whereRef("ArtUserMetadata.userId", "=", "User.id"), + ), ) .as("artCount"), ]) diff --git a/patches/i18next@26.3.6.patch b/patches/i18next@26.3.6.patch new file mode 100644 index 000000000..0da41f0ee --- /dev/null +++ b/patches/i18next@26.3.6.patch @@ -0,0 +1,156 @@ +diff --git a/dist/cjs/i18next.js b/dist/cjs/i18next.js +index 6cdf84263936193ff243df98546da3e9104ab24c..1169f1f82abd621a40f8bc75bc62374d24e73a1d 100644 +--- a/dist/cjs/i18next.js ++++ b/dist/cjs/i18next.js +@@ -531,6 +531,41 @@ class Translator extends EventEmitter { + }; + } + translate(keys, o, lastKey) { ++ if (lastKey === undefined && typeof keys === 'string') { ++ let ck = null; ++ if (o === undefined) { ++ ck = `${this.language}\u0001\u0001\u0001\u0001${keys}`; ++ } else if (typeof o === 'object' && o !== null) { ++ let cacheable = true; ++ for (const k in o) { ++ if (k !== 'lng' && k !== 'lngs' && k !== 'ns' && k !== 'keyPrefix' && o[k] !== undefined) { ++ cacheable = false; ++ break; ++ } ++ } ++ if (cacheable) ck = `${o.lng || this.language}\u0001${o.lngs || ''}\u0001${o.ns || ''}\u0001${o.keyPrefix || ''}\u0001${keys}`; ++ } ++ if (ck !== null) { ++ let cache = this.__translationCache; ++ if (cache === undefined) { ++ cache = this.__translationCache = new Map(); ++ const clear = () => this.__translationCache.clear(); ++ this.resourceStore?.on?.('added', clear); ++ this.resourceStore?.on?.('removed', clear); ++ } ++ const hit = cache.get(ck); ++ if (hit !== undefined) return hit; ++ const res = this.__translateUncached(keys, o, lastKey); ++ if (typeof res === 'string') { ++ if (cache.size >= 40000) cache.clear(); ++ cache.set(ck, res); ++ } ++ return res; ++ } ++ } ++ return this.__translateUncached(keys, o, lastKey); ++ } ++ __translateUncached(keys, o, lastKey) { + let opt = typeof o === 'object' ? { + ...o + } : o; +@@ -2049,21 +2084,24 @@ class I18n extends EventEmitter { + const explicitCallNs = o.ns !== undefined && o.ns !== null; + o.ns = o.ns || fixedT.ns; + if (o.keyPrefix !== '') o.keyPrefix = o.keyPrefix || keyPrefix || fixedT.keyPrefix; +- const selectorOpts = { +- ...this.options, +- ...o ++ const buildSelectorOpts = () => { ++ const selectorOpts = { ++ ...this.options, ++ ...o ++ }; ++ if (Array.isArray(scopeNs) && !explicitCallNs) selectorOpts.ns = scopeNs; ++ return selectorOpts; + }; +- if (Array.isArray(scopeNs) && !explicitCallNs) selectorOpts.ns = scopeNs; +- if (typeof o.keyPrefix === 'function') o.keyPrefix = keysFromSelector(o.keyPrefix, selectorOpts); ++ if (typeof o.keyPrefix === 'function') o.keyPrefix = keysFromSelector(o.keyPrefix, buildSelectorOpts()); + const keySeparator = this.options.keySeparator || '.'; + let resultKey; + if (o.keyPrefix && Array.isArray(key)) { + resultKey = key.map(k => { +- if (typeof k === 'function') k = keysFromSelector(k, selectorOpts); ++ if (typeof k === 'function') k = keysFromSelector(k, buildSelectorOpts()); + return `${o.keyPrefix}${keySeparator}${k}`; + }); + } else { +- if (typeof key === 'function') key = keysFromSelector(key, selectorOpts); ++ if (typeof key === 'function') key = keysFromSelector(key, buildSelectorOpts()); + resultKey = o.keyPrefix ? `${o.keyPrefix}${keySeparator}${key}` : key; + } + return this.t(resultKey, o); +diff --git a/dist/esm/i18next.js b/dist/esm/i18next.js +index d01bbdbd1c0c4a4ebf99d64f5fc61c290be67fc6..dbd2eded184a5e3bb6411a6f7a4afd9e368cb1f0 100644 +--- a/dist/esm/i18next.js ++++ b/dist/esm/i18next.js +@@ -529,6 +529,41 @@ class Translator extends EventEmitter { + }; + } + translate(keys, o, lastKey) { ++ if (lastKey === undefined && typeof keys === 'string') { ++ let ck = null; ++ if (o === undefined) { ++ ck = `${this.language}\u0001\u0001\u0001\u0001${keys}`; ++ } else if (typeof o === 'object' && o !== null) { ++ let cacheable = true; ++ for (const k in o) { ++ if (k !== 'lng' && k !== 'lngs' && k !== 'ns' && k !== 'keyPrefix' && o[k] !== undefined) { ++ cacheable = false; ++ break; ++ } ++ } ++ if (cacheable) ck = `${o.lng || this.language}\u0001${o.lngs || ''}\u0001${o.ns || ''}\u0001${o.keyPrefix || ''}\u0001${keys}`; ++ } ++ if (ck !== null) { ++ let cache = this.__translationCache; ++ if (cache === undefined) { ++ cache = this.__translationCache = new Map(); ++ const clear = () => this.__translationCache.clear(); ++ this.resourceStore?.on?.('added', clear); ++ this.resourceStore?.on?.('removed', clear); ++ } ++ const hit = cache.get(ck); ++ if (hit !== undefined) return hit; ++ const res = this.__translateUncached(keys, o, lastKey); ++ if (typeof res === 'string') { ++ if (cache.size >= 40000) cache.clear(); ++ cache.set(ck, res); ++ } ++ return res; ++ } ++ } ++ return this.__translateUncached(keys, o, lastKey); ++ } ++ __translateUncached(keys, o, lastKey) { + let opt = typeof o === 'object' ? { + ...o + } : o; +@@ -2047,21 +2082,24 @@ class I18n extends EventEmitter { + const explicitCallNs = o.ns !== undefined && o.ns !== null; + o.ns = o.ns || fixedT.ns; + if (o.keyPrefix !== '') o.keyPrefix = o.keyPrefix || keyPrefix || fixedT.keyPrefix; +- const selectorOpts = { +- ...this.options, +- ...o ++ const buildSelectorOpts = () => { ++ const selectorOpts = { ++ ...this.options, ++ ...o ++ }; ++ if (Array.isArray(scopeNs) && !explicitCallNs) selectorOpts.ns = scopeNs; ++ return selectorOpts; + }; +- if (Array.isArray(scopeNs) && !explicitCallNs) selectorOpts.ns = scopeNs; +- if (typeof o.keyPrefix === 'function') o.keyPrefix = keysFromSelector(o.keyPrefix, selectorOpts); ++ if (typeof o.keyPrefix === 'function') o.keyPrefix = keysFromSelector(o.keyPrefix, buildSelectorOpts()); + const keySeparator = this.options.keySeparator || '.'; + let resultKey; + if (o.keyPrefix && Array.isArray(key)) { + resultKey = key.map(k => { +- if (typeof k === 'function') k = keysFromSelector(k, selectorOpts); ++ if (typeof k === 'function') k = keysFromSelector(k, buildSelectorOpts()); + return `${o.keyPrefix}${keySeparator}${k}`; + }); + } else { +- if (typeof key === 'function') key = keysFromSelector(key, selectorOpts); ++ if (typeof key === 'function') key = keysFromSelector(key, buildSelectorOpts()); + resultKey = o.keyPrefix ? `${o.keyPrefix}${keySeparator}${key}` : key; + } + return this.t(resultKey, o); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b005e134..be273b285 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ settings: patchedDependencies: '@react-router/serve@8.1.0': 0073d0acad5d92eea7ef30ed24d736df9f4d59e204f8358ecb3b06b9ea9d8738 '@techstark/opencv-js@5.0.0-release.1': 0e23b4243e8e2f88294c00eefdf8dfc3624f59442bd0c8d075be89a8136144f3 + i18next@26.3.6: 34c245cebb3f9c7861d2576a3a44228364cb898037f86b23a5bbf8b20f55fb1a kysely@0.29.0: a3e94339939b1be5b70610601e96fff19ed5678aab525de24b52dfc5212c686a react-i18next@17.0.11: 1f928881d134daf116e494299036454a3c311fe6fb81f60ef8d158f27466a269 @@ -82,7 +83,7 @@ importers: version: 4.0.3 i18next: specifier: 26.3.6 - version: 26.3.6(typescript@7.0.2) + version: 26.3.6(patch_hash=34c245cebb3f9c7861d2576a3a44228364cb898037f86b23a5bbf8b20f55fb1a)(typescript@7.0.2) i18next-browser-languagedetector: specifier: 8.2.1 version: 8.2.1 @@ -151,7 +152,7 @@ importers: version: 7.2.4(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react-i18next: specifier: 17.0.11 - version: 17.0.11(patch_hash=1f928881d134daf116e494299036454a3c311fe6fb81f60ef8d158f27466a269)(i18next@26.3.6(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) + version: 17.0.11(patch_hash=1f928881d134daf116e494299036454a3c311fe6fb81f60ef8d158f27466a269)(i18next@26.3.6(patch_hash=34c245cebb3f9c7861d2576a3a44228364cb898037f86b23a5bbf8b20f55fb1a)(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) react-router: specifier: 8.3.0 version: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -169,7 +170,7 @@ importers: version: 3.4.1(remix-auth@4.2.0) remix-i18next: specifier: 8.0.0 - version: 8.0.0(i18next@26.3.6(typescript@7.0.2))(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)) + version: 8.0.0(i18next@26.3.6(patch_hash=34c245cebb3f9c7861d2576a3a44228364cb898037f86b23a5bbf8b20f55fb1a)(typescript@7.0.2))(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)) slugify: specifier: 1.6.9 version: 1.6.9 @@ -7680,7 +7681,7 @@ snapshots: picomatch: 4.0.4 yargs: 17.7.2 - i18next@26.3.6(typescript@7.0.2): + i18next@26.3.6(patch_hash=34c245cebb3f9c7861d2576a3a44228364cb898037f86b23a5bbf8b20f55fb1a)(typescript@7.0.2): optionalDependencies: typescript: 7.0.2 @@ -8264,11 +8265,11 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - react-i18next@17.0.11(patch_hash=1f928881d134daf116e494299036454a3c311fe6fb81f60ef8d158f27466a269)(i18next@26.3.6(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2): + react-i18next@17.0.11(patch_hash=1f928881d134daf116e494299036454a3c311fe6fb81f60ef8d158f27466a269)(i18next@26.3.6(patch_hash=34c245cebb3f9c7861d2576a3a44228364cb898037f86b23a5bbf8b20f55fb1a)(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2): dependencies: '@babel/runtime': 7.29.7 html-parse-stringify: 4.0.1 - i18next: 26.3.6(typescript@7.0.2) + i18next: 26.3.6(patch_hash=34c245cebb3f9c7861d2576a3a44228364cb898037f86b23a5bbf8b20f55fb1a)(typescript@7.0.2) react: 19.2.8 use-sync-external-store: 1.6.0(react@19.2.8) optionalDependencies: @@ -8350,9 +8351,9 @@ snapshots: remix-auth@4.2.0: {} - remix-i18next@8.0.0(i18next@26.3.6(typescript@7.0.2))(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)): + remix-i18next@8.0.0(i18next@26.3.6(patch_hash=34c245cebb3f9c7861d2576a3a44228364cb898037f86b23a5bbf8b20f55fb1a)(typescript@7.0.2))(react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)): dependencies: - i18next: 26.3.6(typescript@7.0.2) + i18next: 26.3.6(patch_hash=34c245cebb3f9c7861d2576a3a44228364cb898037f86b23a5bbf8b20f55fb1a)(typescript@7.0.2) react-router: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) require-directory@2.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index fbd1cf120..ebb468326 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,5 +5,6 @@ allowBuilds: patchedDependencies: '@react-router/serve@8.1.0': patches/@react-router__serve@8.1.0.patch '@techstark/opencv-js@5.0.0-release.1': patches/@techstark__opencv-js@5.0.0-release.1.patch + i18next@26.3.6: patches/i18next@26.3.6.patch kysely@0.29.0: patches/kysely@0.29.0.patch react-i18next@17.0.11: patches/react-i18next@17.0.11.patch