Skip to content

feat: replace homepage frameworks with fun rotating "n p m x" picks#1616

Open
serhalp wants to merge 9 commits intomainfrom
feat/hourly-npmx-picks
Open

feat: replace homepage frameworks with fun rotating "n p m x" picks#1616
serhalp wants to merge 9 commits intomainfrom
feat/hourly-npmx-picks

Conversation

@serhalp
Copy link
Member

@serhalp serhalp commented Feb 24, 2026

🔗 Linked issue

😶 discussed on Discord

🧭 Context

  • The hardcoded framework package quick list has an unclear purpose. More and more folks are requesting to add their link. It isn't a scalable feature.
  • The below is a subtle homage to the OG npmjs.com (they used to show a random "definition" of "npm" on page load).
  • It's a fun little easter egg that may lead curious users to wonder how the packages are selected... encouraging them to look at our repo, learn about our atproto features, and join our community 😈.
  • It's somewhat of a metaphor for npmx being borne out of the ecosystem.

📚 Description

Show 4 randomly-selected packages whose names contain the letters n, p, m, x, with the matching letter highlighted in bold + accent colour (one distinct colour per letter when no user accent is set).

Screenshot 2026-02-26 at 22 13 32 Screenshot 2026-02-26 at 22 14 52 Screenshot 2026-02-26 at 22 16 38

Selection algorithm:

  1. Fetch top 500 popular packages from the Algolia search index (empty query, default popularity ranking). Algolia doesn't support filtering by name substring (and doesn't know about our social likes), so we filter these results down after the fact (not deprecated, >=10k downloads/30d, modified <2yrs).
  2. For each letter (n, p, m, x):
    1. Take 30 random candidates whose name contains that letter and check their social like count.
    2. If there are candidates with >=5 community likes, keep only those; otherwise, keep all.
    3. Remove any remaining candidates already assigned to another letter in "n p m x".
    4. Randomly pick one remaining candidate.
    5. If there are no remaining candidates, pick the hardcoded default for this letter (nuxt, pnpm, module-replacements, oxfmt).

Results are cached for 1 hour via with SWR, so all users see the same picks for about an hour, and no user ever experiences a cache miss (and Algolia/constellation slowness).

Show 4 randomly-selected packages whose names contain the letters n, p, m, X, with the matching
letter highlighted in bold + accent colour (one distinct colour per letter when no user accent is
set).

Selection algorithm:
1. Fetch top 500 popular packages from the Algolia search index (empty query, default popularity
   ranking). Algolia doesn't support filtering by name substring (and doesn't know about our social
   likes), so we filter these results down after the fact (not deprecated, >=10k downloads/30d,
   modified <2yrs).
2. For each letter (n, p, m, x)
  1. Take 30 random candidates whose name contains that letter and check their social like count.
  2. If there are candidates with >=5 community likes, keep only those; otherwise, keep all.
  3. Randomly pick one remaining candidate.
  4. If there are no remaining candidates, pick the hardcoded default for this letter (nuxt, pnpm,
     module-replacements, oxfmt).

Results are cached for 1 hour via with SWR, so all users see the same picks for about an hour, and
no user ever experiences a cache miss (and Algolia/constellation slowness).
@vercel
Copy link

vercel bot commented Feb 24, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 27, 2026 3:23am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 27, 2026 3:23am
npmx-lunaria Ignored Ignored Feb 27, 2026 3:23am

Request Review

@serhalp serhalp changed the title feat: replace framework pkgs with random "n p m x" picks feat: replace homepage framework packages with fun rotating "n p m x" picks Feb 24, 2026
@serhalp serhalp changed the title feat: replace homepage framework packages with fun rotating "n p m x" picks feat: replace homepage frameworks with fun rotating "n p m x" picks Feb 24, 2026
@github-actions
Copy link

github-actions bot commented Feb 24, 2026

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
lunaria/files/ar-EG.json Localization changed, will be marked as complete. 🔄️
lunaria/files/az-AZ.json Localization changed, will be marked as complete. 🔄️
lunaria/files/bg-BG.json Localization changed, will be marked as complete. 🔄️
lunaria/files/bn-IN.json Localization changed, will be marked as complete. 🔄️
lunaria/files/cs-CZ.json Localization changed, will be marked as complete. 🔄️
lunaria/files/de-DE.json Localization changed, will be marked as complete. 🔄️
lunaria/files/en-GB.json Localization changed, will be marked as complete. 🔄️
lunaria/files/en-US.json Source changed, localizations will be marked as outdated.
lunaria/files/es-419.json Localization changed, will be marked as complete. 🔄️
lunaria/files/es-ES.json Localization changed, will be marked as complete. 🔄️
lunaria/files/fr-FR.json Localization changed, will be marked as complete. 🔄️
lunaria/files/hi-IN.json Localization changed, will be marked as complete. 🔄️
lunaria/files/hu-HU.json Localization changed, will be marked as complete. 🔄️
lunaria/files/id-ID.json Localization changed, will be marked as complete. 🔄️
lunaria/files/it-IT.json Localization changed, will be marked as complete. 🔄️
lunaria/files/ja-JP.json Localization changed, will be marked as complete. 🔄️
lunaria/files/kn-IN.json Localization changed, will be marked as complete. 🔄️
lunaria/files/mr-IN.json Localization changed, will be marked as complete. 🔄️
lunaria/files/nb-NO.json Localization changed, will be marked as complete. 🔄️
lunaria/files/ne-NP.json Localization changed, will be marked as complete. 🔄️
lunaria/files/pl-PL.json Localization changed, will be marked as complete. 🔄️
lunaria/files/pt-BR.json Localization changed, will be marked as complete. 🔄️
lunaria/files/ru-RU.json Localization changed, will be marked as complete. 🔄️
lunaria/files/ta-IN.json Localization changed, will be marked as complete. 🔄️
lunaria/files/te-IN.json Localization changed, will be marked as complete. 🔄️
lunaria/files/uk-UA.json Localization changed, will be marked as complete. 🔄️
lunaria/files/zh-CN.json Localization changed, will be marked as complete. 🔄️
lunaria/files/zh-TW.json Localization changed, will be marked as complete. 🔄️
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@codecov
Copy link

codecov bot commented Feb 24, 2026

Codecov Report

❌ Patch coverage is 33.33333% with 2 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/pages/index.vue 33.33% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@graphieros
Copy link
Contributor

Since 4 packages are displayed, I think the original width of the wrapper could be restored (#1591 reduced max-w to wrap the long list)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

This pull request replaces the homepage's static framework showcase with a dynamic picks system. A new server API endpoint at server/api/picks.get.ts fetches package data from Algolia, filters by deprecation status and activity metrics, and selects one package per letter (n, p, m, x), enriching them with likes data. The app/pages/index.vue template is refactored to fetch and render this dynamic picks data instead of static content. Localisation files across 30+ locales are updated to replace the popular_packages navigation key with npmx_picks. Supporting utilities, types, tests, and mock routes are added to implement the feature.

Possibly related PRs

Suggested reviewers

  • danielroe
  • graphieros
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description clearly explains the motivation, implementation approach, and selection algorithm for replacing the static framework list with dynamically-selected packages containing the letters n, p, m, x.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/hourly-npmx-picks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
modules/runtime/server/cache.ts (1)

249-257: Consider stabilising mock timestamps for deterministic fixtures.

Using one shared timestamp value in this block avoids per-item clock drift and keeps fixture output steadier.

♻️ Suggested tweak
   if (host === algoliaHost && pathname.endsWith('/query')) {
+    const now = new Date().toISOString()
     return {
       data: {
         hits: [
-          { name: 'nuxt', downloadsLast30Days: 500_000, modified: new Date().toISOString() },
-          { name: 'pnpm', downloadsLast30Days: 800_000, modified: new Date().toISOString() },
-          { name: 'express', downloadsLast30Days: 1_000_000, modified: new Date().toISOString() },
-          { name: 'minimatch', downloadsLast30Days: 600_000, modified: new Date().toISOString() },
-          { name: 'next', downloadsLast30Days: 700_000, modified: new Date().toISOString() },
-          { name: 'axios', downloadsLast30Days: 900_000, modified: new Date().toISOString() },
-          { name: 'remix', downloadsLast30Days: 400_000, modified: new Date().toISOString() },
-          { name: 'webpack', downloadsLast30Days: 750_000, modified: new Date().toISOString() },
+          { name: 'nuxt', downloadsLast30Days: 500_000, modified: now },
+          { name: 'pnpm', downloadsLast30Days: 800_000, modified: now },
+          { name: 'express', downloadsLast30Days: 1_000_000, modified: now },
+          { name: 'minimatch', downloadsLast30Days: 600_000, modified: now },
+          { name: 'next', downloadsLast30Days: 700_000, modified: now },
+          { name: 'axios', downloadsLast30Days: 900_000, modified: now },
+          { name: 'remix', downloadsLast30Days: 400_000, modified: now },
+          { name: 'webpack', downloadsLast30Days: 750_000, modified: now },
         ],
       },
     }
   }

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ed4254 and 9e462b5.

📒 Files selected for processing (66)
  • app/pages/index.vue
  • i18n/locales/ar.json
  • i18n/locales/az-AZ.json
  • i18n/locales/bg-BG.json
  • i18n/locales/bn-IN.json
  • i18n/locales/cs-CZ.json
  • i18n/locales/de-DE.json
  • i18n/locales/en.json
  • i18n/locales/es.json
  • i18n/locales/fr-FR.json
  • i18n/locales/hi-IN.json
  • i18n/locales/hu-HU.json
  • i18n/locales/id-ID.json
  • i18n/locales/it-IT.json
  • i18n/locales/ja-JP.json
  • i18n/locales/kn-IN.json
  • i18n/locales/mr-IN.json
  • i18n/locales/nb-NO.json
  • i18n/locales/ne-NP.json
  • i18n/locales/pl-PL.json
  • i18n/locales/pt-BR.json
  • i18n/locales/ru-RU.json
  • i18n/locales/ta-IN.json
  • i18n/locales/te-IN.json
  • i18n/locales/uk-UA.json
  • i18n/locales/zh-CN.json
  • i18n/locales/zh-TW.json
  • i18n/schema.json
  • lunaria/files/ar-EG.json
  • lunaria/files/az-AZ.json
  • lunaria/files/bg-BG.json
  • lunaria/files/bn-IN.json
  • lunaria/files/cs-CZ.json
  • lunaria/files/de-DE.json
  • lunaria/files/en-GB.json
  • lunaria/files/en-US.json
  • lunaria/files/es-419.json
  • lunaria/files/es-ES.json
  • lunaria/files/fr-FR.json
  • lunaria/files/hi-IN.json
  • lunaria/files/hu-HU.json
  • lunaria/files/id-ID.json
  • lunaria/files/it-IT.json
  • lunaria/files/ja-JP.json
  • lunaria/files/kn-IN.json
  • lunaria/files/mr-IN.json
  • lunaria/files/nb-NO.json
  • lunaria/files/ne-NP.json
  • lunaria/files/pl-PL.json
  • lunaria/files/pt-BR.json
  • lunaria/files/ru-RU.json
  • lunaria/files/ta-IN.json
  • lunaria/files/te-IN.json
  • lunaria/files/uk-UA.json
  • lunaria/files/zh-CN.json
  • lunaria/files/zh-TW.json
  • modules/runtime/server/cache.ts
  • nuxt.config.ts
  • server/api/picks.get.ts
  • server/utils/atproto/utils/likes.ts
  • server/utils/picks.ts
  • shared/types/index.ts
  • shared/types/picks.ts
  • test/e2e/homepage-picks.spec.ts
  • test/fixtures/mock-routes.cjs
  • test/unit/server/utils/picks.spec.ts
💤 Files with no reviewable changes (47)
  • lunaria/files/pl-PL.json
  • i18n/locales/pl-PL.json
  • lunaria/files/zh-TW.json
  • lunaria/files/zh-CN.json
  • lunaria/files/bg-BG.json
  • lunaria/files/mr-IN.json
  • i18n/locales/pt-BR.json
  • i18n/locales/ar.json
  • i18n/locales/uk-UA.json
  • i18n/locales/es.json
  • i18n/locales/zh-CN.json
  • lunaria/files/bn-IN.json
  • lunaria/files/ne-NP.json
  • i18n/locales/ja-JP.json
  • i18n/locales/mr-IN.json
  • lunaria/files/az-AZ.json
  • i18n/locales/ta-IN.json
  • i18n/locales/bg-BG.json
  • i18n/locales/hi-IN.json
  • i18n/locales/cs-CZ.json
  • i18n/locales/de-DE.json
  • i18n/locales/nb-NO.json
  • lunaria/files/uk-UA.json
  • lunaria/files/ru-RU.json
  • i18n/locales/ru-RU.json
  • lunaria/files/te-IN.json
  • lunaria/files/id-ID.json
  • i18n/locales/te-IN.json
  • lunaria/files/ta-IN.json
  • i18n/locales/az-AZ.json
  • lunaria/files/es-ES.json
  • i18n/locales/bn-IN.json
  • lunaria/files/ja-JP.json
  • i18n/locales/ne-NP.json
  • lunaria/files/de-DE.json
  • lunaria/files/it-IT.json
  • lunaria/files/hu-HU.json
  • i18n/locales/it-IT.json
  • i18n/locales/hu-HU.json
  • lunaria/files/ar-EG.json
  • lunaria/files/cs-CZ.json
  • i18n/locales/zh-TW.json
  • lunaria/files/hi-IN.json
  • lunaria/files/nb-NO.json
  • i18n/locales/id-ID.json
  • lunaria/files/es-419.json
  • lunaria/files/pt-BR.json

Comment on lines +43 to +58
const algoliaResponse = await $fetch<AlgoliaResponse>(
`https://${appId}-dsn.algolia.net/1/indexes/${indexName}/query`,
{
method: 'POST',
headers: {
'X-Algolia-Application-Id': appId,
'X-Algolia-API-Key': apiKey,
},
body: {
query: '',
hitsPerPage: ALGOLIA_POOL_SIZE,
attributesToRetrieve: ['name', 'downloadsLast30Days', 'modified', 'isDeprecated'],
attributesToHighlight: [],
},
},
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Handle Algolia failures without taking down /api/picks.

At Line 43, a transient upstream failure throws and aborts the handler. This endpoint should degrade gracefully (fallback picks) instead of returning 5xx.

🛡️ Proposed fix
-    const algoliaResponse = await $fetch<AlgoliaResponse>(
-      `https://${appId}-dsn.algolia.net/1/indexes/${indexName}/query`,
-      {
-        method: 'POST',
-        headers: {
-          'X-Algolia-Application-Id': appId,
-          'X-Algolia-API-Key': apiKey,
-        },
-        body: {
-          query: '',
-          hitsPerPage: ALGOLIA_POOL_SIZE,
-          attributesToRetrieve: ['name', 'downloadsLast30Days', 'modified', 'isDeprecated'],
-          attributesToHighlight: [],
-        },
-      },
-    )
+    let algoliaHits: AlgoliaHit[] = []
+    try {
+      const algoliaResponse = await $fetch<AlgoliaResponse>(
+        `https://${appId}-dsn.algolia.net/1/indexes/${indexName}/query`,
+        {
+          method: 'POST',
+          headers: {
+            'X-Algolia-Application-Id': appId,
+            'X-Algolia-API-Key': apiKey,
+          },
+          body: {
+            query: '',
+            hitsPerPage: ALGOLIA_POOL_SIZE,
+            attributesToRetrieve: ['name', 'downloadsLast30Days', 'modified', 'isDeprecated'],
+            attributesToHighlight: [],
+          },
+        },
+      )
+      algoliaHits = algoliaResponse.hits ?? []
+    } catch {
+      algoliaHits = []
+    }
...
-    const pool = algoliaResponse.hits.filter(hit => {
+    const pool = algoliaHits.filter(hit => {

As per coding guidelines: "Use error handling patterns consistently".

Comment on lines +62 to +66
const pool = algoliaResponse.hits.filter(hit => {
if (hit.isDeprecated) return false
if (hit.downloadsLast30Days < MIN_DOWNLOADS_LAST_30_DAYS) return false
if (now - new Date(hit.modified).getTime() > MAX_MODIFIED_AGE_MS) return false
return true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject hits with invalid modified dates before age filtering.

At Line 65, invalid dates yield NaN and currently slip through the filter. Guard parsing explicitly.

🔍 Proposed fix
     const now = Date.now()
     const pool = algoliaResponse.hits.filter(hit => {
       if (hit.isDeprecated) return false
       if (hit.downloadsLast30Days < MIN_DOWNLOADS_LAST_30_DAYS) return false
-      if (now - new Date(hit.modified).getTime() > MAX_MODIFIED_AGE_MS) return false
+      const modifiedAt = Date.parse(hit.modified)
+      if (Number.isNaN(modifiedAt)) return false
+      if (now - modifiedAt > MAX_MODIFIED_AGE_MS) return false
       return true
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const pool = algoliaResponse.hits.filter(hit => {
if (hit.isDeprecated) return false
if (hit.downloadsLast30Days < MIN_DOWNLOADS_LAST_30_DAYS) return false
if (now - new Date(hit.modified).getTime() > MAX_MODIFIED_AGE_MS) return false
return true
const pool = algoliaResponse.hits.filter(hit => {
if (hit.isDeprecated) return false
if (hit.downloadsLast30Days < MIN_DOWNLOADS_LAST_30_DAYS) return false
const modifiedAt = Date.parse(hit.modified)
if (Number.isNaN(modifiedAt)) return false
if (now - modifiedAt > MAX_MODIFIED_AGE_MS) return false
return true
})

Comment on lines +16 to +17
const highlightedLetter = pickItems.nth(i).locator('span.font-bold')
await expect(highlightedLetter).toBeVisible()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the test file
find . -type f -name "homepage-picks.spec.ts" | head -20

Repository: npmx-dev/npmx.dev

Length of output: 94


🏁 Script executed:

# Let's also explore the directory structure to understand the project layout
git ls-files | grep -E "(test|spec|pages|components)" | head -30

Repository: npmx-dev/npmx.dev

Length of output: 1134


🏁 Script executed:

# Read the test file to see the actual code at lines 16-17
cat -n test/e2e/homepage-picks.spec.ts

Repository: npmx-dev/npmx.dev

Length of output: 1719


🏁 Script executed:

# Search for the component that renders the "npmx picks" navigation
rg -i "npmx picks" --type vue --type ts --type tsx

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Also search for where picks are displayed
rg -i "picks" app/components app/pages --type vue | head -20

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Search for "npmx picks" across all files
rg "npmx picks" -i

Repository: npmx-dev/npmx.dev

Length of output: 441


🏁 Script executed:

# Search for the picks navigation/component
rg -i "picks" app/pages app/components --type-add 'vue:*.vue' -tvue

Repository: npmx-dev/npmx.dev

Length of output: 441


🏁 Script executed:

# Read the index.vue file to see how picks are rendered
cat -n app/pages/index.vue | grep -A 20 "picksData?.picks"

Repository: npmx-dev/npmx.dev

Length of output: 1307


🏁 Script executed:

# Let's get the full index.vue file to see the complete picks section
wc -l app/pages/index.vue

Repository: npmx-dev/npmx.dev

Length of output: 84


🏁 Script executed:

# Let's verify the full context of the highlighted letter rendering to confirm
sed -n '120,131p' app/pages/index.vue

Repository: npmx-dev/npmx.dev

Length of output: 578


Selector targets wrong class name — test will fail.

Line 16 uses span.font-bold, but the component renders span class="font-semibold" (line 124 of app/pages/index.vue), so the assertion will not find the element.

The quickest fix is to update the selector to match the actual class:

Corrected selector
-      const highlightedLetter = pickItems.nth(i).locator('span.font-bold')
+      const highlightedLetter = pickItems.nth(i).locator('span.font-semibold')

Alternatively, for greater resilience to future styling changes, add a data-testid="highlighted-letter" to the highlighted span in the component and target that here instead.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const highlightedLetter = pickItems.nth(i).locator('span.font-bold')
await expect(highlightedLetter).toBeVisible()
const highlightedLetter = pickItems.nth(i).locator('span.font-semibold')
await expect(highlightedLetter).toBeVisible()

pattern: 'https://constellation.microcosm.blue/**',
match: matchConstellationApi,
},
{ name: 'Algolia API', pattern: 'https://*-dsn.algolia.net/**', match: matchAlgoliaApi },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Algolia wildcard host pattern will not match in the local route matcher.

At Line 571, https://*-dsn.algolia.net/** cannot match via urlMatchesPattern() because that matcher only does startsWith prefix checks for /** patterns. This makes Algolia calls effectively unmocked when matchRoute() is used.

🔧 Proposed fix
 function urlMatchesPattern(url, pattern) {
-  // Convert "https://example.com/**" to a prefix check
-  if (pattern.endsWith('/**')) {
-    const prefix = pattern.slice(0, -2)
-    return url.startsWith(prefix)
-  }
-  return url === pattern
+  // Support wildcard patterns (e.g. https://*-dsn.algolia.net/**)
+  if (pattern.includes('*')) {
+    const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')
+    return new RegExp(`^${escaped}$`).test(url)
+  }
+  if (pattern.endsWith('/**')) {
+    const prefix = pattern.slice(0, -2)
+    return url.startsWith(prefix)
+  }
+  return url === pattern
 }

const PICKS_MAX_AGE_MS = 60 * 60 * 1000

/** Pick `n` random items from `arr` (Fisher-Yates on a copy, sliced). */
function randomSample<T>(arr: T[], n: number): T[] {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't seem worth pulling in a dependency for this. AFAIK this is as simple as it gets without caring about cryptographically secure randomness or anything like that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants