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:
Generated
+87
-1
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user