Use REDIS_URL for share shortlinks.

Stores /s/:code mappings in the connected Redis database and falls back to Vercel KV REST when available.
This commit is contained in:
b1rdmania
2025-12-26 15:36:00 +00:00
parent d9861f8bbf
commit a23e348d5e
3 changed files with 138 additions and 21 deletions
+87 -1
View File
@@ -13,7 +13,8 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"express": "^4.18.2", "express": "^4.18.2",
"midi-writer-js": "^3.1.1" "midi-writer-js": "^3.1.1",
"redis": "^5.10.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
@@ -465,6 +466,66 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@redis/bloom": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.10.0.tgz",
"integrity": "sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.10.0"
}
},
"node_modules/@redis/client": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.10.0.tgz",
"integrity": "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA==",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@redis/json": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.10.0.tgz",
"integrity": "sha512-B2G8XlOmTPUuZtD44EMGbtoepQG34RCDXLZbjrtON1Djet0t5Ri7/YPXvL9aomXqP8lLTreaprtyLKF4tmXEEA==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.10.0"
}
},
"node_modules/@redis/search": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.10.0.tgz",
"integrity": "sha512-3SVcPswoSfp2HnmWbAGUzlbUPn7fOohVu2weUQ0S+EMiQi8jwjL+aN2p6V3TI65eNfVsJ8vyPvqWklm6H6esmg==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.10.0"
}
},
"node_modules/@redis/time-series": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.10.0.tgz",
"integrity": "sha512-cPkpddXH5kc/SdRhF0YG0qtjL+noqFT0AcHbQ6axhsPsO7iqPi1cjxgdkE9TNeKiBUUdCaU1DbqkR/LzbzPBhg==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@redis/client": "^5.10.0"
}
},
"node_modules/@tonaljs/midi": { "node_modules/@tonaljs/midi": {
"version": "4.10.2", "version": "4.10.2",
"resolved": "https://registry.npmjs.org/@tonaljs/midi/-/midi-4.10.2.tgz", "resolved": "https://registry.npmjs.org/@tonaljs/midi/-/midi-4.10.2.tgz",
@@ -779,6 +840,15 @@
"url": "https://github.com/sponsors/fb55" "url": "https://github.com/sponsors/fb55"
} }
}, },
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -1624,6 +1694,22 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/redis": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-5.10.0.tgz",
"integrity": "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw==",
"license": "MIT",
"dependencies": {
"@redis/bloom": "5.10.0",
"@redis/client": "5.10.0",
"@redis/json": "5.10.0",
"@redis/search": "5.10.0",
"@redis/time-series": "5.10.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/resolve-pkg-maps": { "node_modules/resolve-pkg-maps": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+2 -1
View File
@@ -15,7 +15,8 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"express": "^4.18.2", "express": "^4.18.2",
"midi-writer-js": "^3.1.1" "midi-writer-js": "^3.1.1",
"redis": "^5.10.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
+43 -13
View File
@@ -2,6 +2,7 @@ import express from 'express';
import cors from 'cors'; import cors from 'cors';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { kv } from '@vercel/kv'; import { kv } from '@vercel/kv';
import { createClient } from 'redis';
import { MIDISearchService } from './services/MIDISearchService.js'; import { MIDISearchService } from './services/MIDISearchService.js';
import { MIDIFetchService } from './services/MIDIFetchService.js'; import { MIDIFetchService } from './services/MIDIFetchService.js';
import { MIDIParseService } from './services/MIDIParseService.js'; import { MIDIParseService } from './services/MIDIParseService.js';
@@ -35,6 +36,28 @@ type SharePayload =
const localShareStore = new Map<string, SharePayload>(); const localShareStore = new Map<string, SharePayload>();
const isVercel = Boolean(process.env.VERCEL); const isVercel = Boolean(process.env.VERCEL);
const hasKvEnv = Boolean(process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN); const hasKvEnv = Boolean(process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN);
const redisUrl = process.env.REDIS_URL;
const hasRedisEnv = Boolean(redisUrl);
let redisClient: ReturnType<typeof createClient> | null = null;
let redisConnectPromise: Promise<void> | null = null;
async function getRedis(): Promise<ReturnType<typeof createClient>> {
if (!redisUrl) throw new Error('REDIS_URL is not configured for this project.');
if (!redisClient) {
redisClient = createClient({ url: redisUrl });
redisClient.on('error', (err) => console.error('Redis error:', err));
}
if (!redisClient.isOpen) {
if (!redisConnectPromise) {
redisConnectPromise = redisClient.connect().then(() => {}).finally(() => {
redisConnectPromise = null;
});
}
await redisConnectPromise;
}
return redisClient;
}
function base62(bytes: Uint8Array): string { function base62(bytes: Uint8Array): string {
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
@@ -52,37 +75,44 @@ async function shareSet(code: string, payload: SharePayload): Promise<void> {
const key = `share:${code}`; const key = `share:${code}`;
// 30 days TTL // 30 days TTL
const exSec = 60 * 60 * 24 * 30; const exSec = 60 * 60 * 24 * 30;
if (isVercel && !hasKvEnv) { if (isVercel && !hasRedisEnv && !hasKvEnv) {
throw new Error('Vercel KV is not configured for this project.'); throw new Error('No storage configured (need REDIS_URL or Vercel KV env vars).');
} }
try { // Prefer Redis integration if present (your project has REDIS_URL).
if (hasRedisEnv) {
const r = await getRedis();
await r.set(key, JSON.stringify(payload), { EX: exSec });
return;
}
// Fallback: Vercel KV REST (Upstash)
if (hasKvEnv) { if (hasKvEnv) {
await kv.set(key, payload, { ex: exSec }); await kv.set(key, payload, { ex: exSec });
return; return;
} }
} catch {
// fall through to local store (dev only)
}
// Local dev: best-effort in-memory store. // Local dev only: best-effort in-memory store.
localShareStore.set(code, payload); localShareStore.set(code, payload);
} }
async function shareGet(code: string): Promise<SharePayload | null> { async function shareGet(code: string): Promise<SharePayload | null> {
const key = `share:${code}`; const key = `share:${code}`;
if (isVercel && !hasKvEnv) { if (isVercel && !hasRedisEnv && !hasKvEnv) {
throw new Error('Vercel KV is not configured for this project.'); throw new Error('No storage configured (need REDIS_URL or Vercel KV env vars).');
}
if (hasRedisEnv) {
const r = await getRedis();
const val = await r.get(key);
if (!val) return null;
return JSON.parse(val) as SharePayload;
} }
try {
if (hasKvEnv) { if (hasKvEnv) {
const val = await kv.get<SharePayload>(key); const val = await kv.get<SharePayload>(key);
return val ?? null; return val ?? null;
} }
} catch {
// fall through to local store (dev only)
}
return localShareStore.get(code) ?? null; return localShareStore.get(code) ?? null;
} }