mirror of
https://github.com/ld3z/new-manga.git
synced 2026-11-11 06:23:46 -07:00
Compare commits
5 commits
90f0e2417e
...
6d783ad0d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d783ad0d0 | ||
|
|
e48ea08c05 | ||
|
|
7577ff4f83 | ||
|
|
5874ae5d9b | ||
|
|
0fd7ea433d |
6 changed files with 410 additions and 65 deletions
BIN
public/cynthia1.webp
Normal file
BIN
public/cynthia1.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 629 KiB |
BIN
public/darling.webp
Normal file
BIN
public/darling.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
BIN
public/granny.webp
Normal file
BIN
public/granny.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 817 KiB |
BIN
public/numi_lily.webp
Normal file
BIN
public/numi_lily.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
|
|
@ -33,7 +33,7 @@ export const backgrounds: BackgroundImage[] = [
|
||||||
source: "https://danbooru.donmai.us/posts/6900673",
|
source: "https://danbooru.donmai.us/posts/6900673",
|
||||||
socials: [
|
socials: [
|
||||||
{
|
{
|
||||||
name: "Twitter",
|
name: "Artist's Twitter",
|
||||||
link: "https://twitter.com/ayanagi0319",
|
link: "https://twitter.com/ayanagi0319",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -45,7 +45,7 @@ export const backgrounds: BackgroundImage[] = [
|
||||||
source: "https://danbooru.donmai.us/posts/8743208",
|
source: "https://danbooru.donmai.us/posts/8743208",
|
||||||
socials: [
|
socials: [
|
||||||
{
|
{
|
||||||
name: "Twitter",
|
name: "Artist's Twitter",
|
||||||
link: "https://x.com/kamepan44231",
|
link: "https://x.com/kamepan44231",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -57,7 +57,7 @@ export const backgrounds: BackgroundImage[] = [
|
||||||
source: "https://danbooru.donmai.us/posts/8074444",
|
source: "https://danbooru.donmai.us/posts/8074444",
|
||||||
socials: [
|
socials: [
|
||||||
{
|
{
|
||||||
name: "Twitter",
|
name: "Artist's Twitter",
|
||||||
link: "https://x.com/Ten_0123_/",
|
link: "https://x.com/Ten_0123_/",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -69,8 +69,8 @@ export const backgrounds: BackgroundImage[] = [
|
||||||
source: "https://www.pixiv.net/en/artworks/124222053",
|
source: "https://www.pixiv.net/en/artworks/124222053",
|
||||||
socials: [
|
socials: [
|
||||||
{
|
{
|
||||||
name: "Twitter",
|
name: "Artist's Twitter",
|
||||||
link: "https://twitter.com/greatodoggo",
|
link: "https://x.com/greatodoggo",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -81,7 +81,7 @@ export const backgrounds: BackgroundImage[] = [
|
||||||
source: "https://safebooru.org/index.php?page=post&s=view&id=5418793",
|
source: "https://safebooru.org/index.php?page=post&s=view&id=5418793",
|
||||||
socials: [
|
socials: [
|
||||||
{
|
{
|
||||||
name: "Twitter",
|
name: "Artist's Twitter",
|
||||||
link: "https://x.com/soaphisoap",
|
link: "https://x.com/soaphisoap",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -93,7 +93,7 @@ export const backgrounds: BackgroundImage[] = [
|
||||||
source: "https://safebooru.org/index.php?page=post&s=view&id=5531630",
|
source: "https://safebooru.org/index.php?page=post&s=view&id=5531630",
|
||||||
socials: [
|
socials: [
|
||||||
{
|
{
|
||||||
name: "Twitter",
|
name: "Artist's Twitter",
|
||||||
link: "https://x.com/charlieinoni",
|
link: "https://x.com/charlieinoni",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -106,11 +106,59 @@ export const backgrounds: BackgroundImage[] = [
|
||||||
"https://i.pximg.net/img-original/img/2025/03/03/15/13/24/127822472_p0.png",
|
"https://i.pximg.net/img-original/img/2025/03/03/15/13/24/127822472_p0.png",
|
||||||
socials: [
|
socials: [
|
||||||
{
|
{
|
||||||
name: "Twitter",
|
name: "Artist's Twitter",
|
||||||
link: "https://x.com/",
|
link: "https://x.com/",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
url: "/cynthia1.webp",
|
||||||
|
title: "Cynthia (Pokemon)",
|
||||||
|
artist: "???",
|
||||||
|
source: "https://safebooru.org/index.php?page=post&s=view&id=5547740",
|
||||||
|
socials: [
|
||||||
|
{
|
||||||
|
name: "Artist's Twitter",
|
||||||
|
link: "https://x.com/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/darling.webp",
|
||||||
|
title: "Zero Two",
|
||||||
|
artist: "AWZ",
|
||||||
|
source: "https://www.pixiv.net/en/artworks/123110068",
|
||||||
|
socials: [
|
||||||
|
{
|
||||||
|
name: "Pixiv Profile",
|
||||||
|
link: "https://www.pixiv.net/en/users/67355371",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/granny.webp",
|
||||||
|
title: "Turbo Granny (Dandadan)",
|
||||||
|
artist: "MajimartBCN",
|
||||||
|
source: "https://twitter.com/MajimartBCN/status/1864741369383694841",
|
||||||
|
socials: [
|
||||||
|
{
|
||||||
|
name: "Artist's Twitter",
|
||||||
|
link: "https://x.com/MajimartBCN",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/numi_lily.webp",
|
||||||
|
title: "Nihmune & Shylily",
|
||||||
|
artist: "Gryever_AW",
|
||||||
|
source: "https://x.com/gryever_aw/status/1890772686084686118",
|
||||||
|
socials: [
|
||||||
|
{
|
||||||
|
name: "Artist's Twitter",
|
||||||
|
link: "https://x.com/Gryever_AW",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// Add more backgrounds here!
|
// Add more backgrounds here!
|
||||||
// {
|
// {
|
||||||
// url: 'your-image-url',
|
// url: 'your-image-url',
|
||||||
|
|
|
||||||
375
src/lib/db.ts
375
src/lib/db.ts
|
|
@ -7,6 +7,10 @@ export const CHAPTER_CACHE_TTL = 60 * 60; // 1 hour
|
||||||
const MAX_RETRIES = 3;
|
const MAX_RETRIES = 3;
|
||||||
const BASE_DELAY_MS = 50;
|
const BASE_DELAY_MS = 50;
|
||||||
const IS_VERCEL = process.env.VERCEL || false;
|
const IS_VERCEL = process.env.VERCEL || false;
|
||||||
|
const HEALTH_CHECK_INTERVAL = 60 * 1000; // Check primary health every minute
|
||||||
|
const REPLICA_PROMOTION_THRESHOLD = 5; // After 5 failed primary health checks, promote a replica to primary
|
||||||
|
const SYNC_BUFFER_SIZE = 100; // Maximum number of operations to buffer before syncing
|
||||||
|
const SYNC_INTERVAL = 10 * 1000; // Sync replicas every 10 seconds
|
||||||
|
|
||||||
// Redis URLs from environment variables
|
// Redis URLs from environment variables
|
||||||
const PRIMARY_REDIS_URL =
|
const PRIMARY_REDIS_URL =
|
||||||
|
|
@ -18,13 +22,35 @@ const REPLICA_URLS = [
|
||||||
import.meta.env.REPLICA_REDIS_URL_1 ||
|
import.meta.env.REPLICA_REDIS_URL_1 ||
|
||||||
process.env.REPLICA_REDIS_URL_1 ||
|
process.env.REPLICA_REDIS_URL_1 ||
|
||||||
import.meta.env.REPLICA_REDIS_URL,
|
import.meta.env.REPLICA_REDIS_URL,
|
||||||
import.meta.env.REPLICA_REDIS_URL_2 || process.env.REPLICA_REDIS_URL_2,
|
import.meta.env.REPLICA_REDIS_URL_2 || process.env.REPLICA_REPLICA_URL_2 || process.env.REPLICA_REDIS_URL_2,
|
||||||
import.meta.env.REPLICA_REDIS_URL_3 || process.env.REPLICA_REDIS_URL_3,
|
import.meta.env.REPLICA_REDIS_URL_3 || process.env.REPLICA_REDIS_URL_3,
|
||||||
].filter(Boolean) as string[];
|
].filter(Boolean) as string[];
|
||||||
|
|
||||||
// Singleton clients
|
// Log available Redis instances
|
||||||
|
console.log(`Primary Redis URL: ${PRIMARY_REDIS_URL ? 'Available' : 'Not configured'}`);
|
||||||
|
console.log(`Replica URLs available: ${REPLICA_URLS.length}`);
|
||||||
|
|
||||||
|
// Singleton clients with health status tracking
|
||||||
let primaryClient: Redis | null = null;
|
let primaryClient: Redis | null = null;
|
||||||
|
let primaryHealthy: boolean = true;
|
||||||
|
let primaryFailedChecks = 0;
|
||||||
let replicaClients: Map<string, Redis> = new Map();
|
let replicaClients: Map<string, Redis> = new Map();
|
||||||
|
let activeReplicaUrl: string | null = null;
|
||||||
|
|
||||||
|
// Track which replica was used when primary was down
|
||||||
|
let lastUsedReplica: string | null = null;
|
||||||
|
|
||||||
|
// Operation buffer for sync
|
||||||
|
interface RedisOperation {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
ttl?: number;
|
||||||
|
type: 'set' | 'del';
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer to store operations that need to be synced
|
||||||
|
const operationBuffer: RedisOperation[] = [];
|
||||||
|
|
||||||
// Redis instance naming for logging
|
// Redis instance naming for logging
|
||||||
const redisInstanceNames = new Map<string, string>();
|
const redisInstanceNames = new Map<string, string>();
|
||||||
|
|
@ -57,6 +83,7 @@ async function createRedisClient(url: string): Promise<Redis> {
|
||||||
throw new Error("Redis URL not provided");
|
throw new Error("Redis URL not provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Creating Redis client for ${getRedisName(url)}: ${url.slice(0, 20)}...`);
|
||||||
const redisName = getRedisName(url);
|
const redisName = getRedisName(url);
|
||||||
|
|
||||||
const client = new Redis(url, {
|
const client = new Redis(url, {
|
||||||
|
|
@ -82,14 +109,66 @@ async function createRedisClient(url: string): Promise<Redis> {
|
||||||
|
|
||||||
// Setup event listeners
|
// Setup event listeners
|
||||||
client
|
client
|
||||||
.on("ready", () => console.log(`${redisName} ready`))
|
.on("ready", () => {
|
||||||
.on("error", (err) => console.error(`${redisName} error:`, err))
|
console.log(`${redisName} ready`);
|
||||||
|
|
||||||
|
// If primary recovers and we had used a replica, initiate sync from replica to primary
|
||||||
|
if (url === PRIMARY_REDIS_URL && lastUsedReplica) {
|
||||||
|
syncFromReplicaToPrimary(lastUsedReplica).catch(err =>
|
||||||
|
console.error(`Failed to sync from replica to primary: ${err}`));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
console.error(`${redisName} error:`, err);
|
||||||
|
if (url === PRIMARY_REDIS_URL) {
|
||||||
|
primaryHealthy = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
.on("reconnecting", () => console.log(`${redisName} reconnecting`))
|
.on("reconnecting", () => console.log(`${redisName} reconnecting`))
|
||||||
.on("end", () => console.log(`${redisName} closed`));
|
.on("end", () => {
|
||||||
|
console.log(`${redisName} closed`);
|
||||||
|
if (url === PRIMARY_REDIS_URL) {
|
||||||
|
primaryHealthy = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize all replicas to establish connections early
|
||||||
|
*/
|
||||||
|
async function initializeAllClients(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Initialize primary
|
||||||
|
if (PRIMARY_REDIS_URL && (!primaryClient || primaryClient.status !== "ready")) {
|
||||||
|
primaryClient = await createRedisClient(PRIMARY_REDIS_URL);
|
||||||
|
primaryHealthy = await checkConnection(primaryClient, "Primary");
|
||||||
|
console.log(`Primary Redis initialized, healthy: ${primaryHealthy}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize all replicas
|
||||||
|
for (const url of REPLICA_URLS) {
|
||||||
|
if (!replicaClients.has(url)) {
|
||||||
|
try {
|
||||||
|
const client = await createRedisClient(url);
|
||||||
|
const isHealthy = await checkConnection(client, getRedisName(url));
|
||||||
|
if (isHealthy) {
|
||||||
|
replicaClients.set(url, client);
|
||||||
|
console.log(`Replica ${getRedisName(url)} initialized successfully`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to initialize replica ${getRedisName(url)}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Initialized ${replicaClients.size} replica connections`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during client initialization:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a Redis client is connected and responsive
|
* Check if a Redis client is connected and responsive
|
||||||
*/
|
*/
|
||||||
|
|
@ -110,19 +189,42 @@ async function checkConnection(client: Redis, name: string): Promise<boolean> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an active Redis client from the pool
|
* Get an active Redis client from the pool
|
||||||
|
* Prioritizes primary, falls back to replicas
|
||||||
*/
|
*/
|
||||||
export async function getRedisClient(): Promise<Redis> {
|
export async function getRedisClient(): Promise<Redis> {
|
||||||
// Try primary client
|
// Try primary client first if it's considered healthy
|
||||||
if (!primaryClient || primaryClient.status !== "ready") {
|
if (primaryHealthy && (primaryClient === null || primaryClient.status !== "ready")) {
|
||||||
|
try {
|
||||||
primaryClient = await createRedisClient(PRIMARY_REDIS_URL!);
|
primaryClient = await createRedisClient(PRIMARY_REDIS_URL!);
|
||||||
|
const isPrimaryConnected = await checkConnection(primaryClient, "Primary");
|
||||||
|
primaryHealthy = isPrimaryConnected;
|
||||||
|
if (isPrimaryConnected) {
|
||||||
|
primaryFailedChecks = 0;
|
||||||
|
console.log("Initialized primary Redis connection");
|
||||||
|
return primaryClient;
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
if (await checkConnection(primaryClient, "Primary")) {
|
console.error("Failed to initialize primary Redis:", err);
|
||||||
|
primaryHealthy = false;
|
||||||
|
primaryFailedChecks++;
|
||||||
|
}
|
||||||
|
} else if (primaryHealthy && primaryClient && primaryClient.status === "ready") {
|
||||||
return primaryClient;
|
return primaryClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try replicas
|
// Primary is unhealthy, try to use the active replica if we have one
|
||||||
|
if (activeReplicaUrl && replicaClients.has(activeReplicaUrl)) {
|
||||||
|
const activeReplica = replicaClients.get(activeReplicaUrl)!;
|
||||||
|
if (activeReplica.status === "ready" && await checkConnection(activeReplica, getRedisName(activeReplicaUrl))) {
|
||||||
|
console.log(`Using active replica: ${getRedisName(activeReplicaUrl)}`);
|
||||||
|
lastUsedReplica = activeReplicaUrl; // Track that we used this replica
|
||||||
|
return activeReplica;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No active replica, try replicas in order
|
||||||
for (const url of REPLICA_URLS) {
|
for (const url of REPLICA_URLS) {
|
||||||
|
try {
|
||||||
let client = replicaClients.get(url);
|
let client = replicaClients.get(url);
|
||||||
if (!client || client.status !== "ready") {
|
if (!client || client.status !== "ready") {
|
||||||
client = await createRedisClient(url);
|
client = await createRedisClient(url);
|
||||||
|
|
@ -130,49 +232,214 @@ export async function getRedisClient(): Promise<Redis> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await checkConnection(client, getRedisName(url))) {
|
if (await checkConnection(client, getRedisName(url))) {
|
||||||
|
console.log(`Switching to replica: ${getRedisName(url)}`);
|
||||||
|
activeReplicaUrl = url;
|
||||||
|
lastUsedReplica = url; // Track that we used this replica
|
||||||
|
|
||||||
|
// If primary has been down for a while, promote this replica to be the new primary
|
||||||
|
if (primaryFailedChecks >= REPLICA_PROMOTION_THRESHOLD) {
|
||||||
|
console.log(`Promoting ${getRedisName(url)} to primary after ${primaryFailedChecks} failed checks`);
|
||||||
|
primaryClient = client;
|
||||||
|
primaryHealthy = true;
|
||||||
|
primaryFailedChecks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to connect to replica ${getRedisName(url)}:`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Force new primary connection
|
// If all else fails, try one more time with primary
|
||||||
|
console.warn("All Redis connections failed, making last attempt with primary");
|
||||||
primaryClient = await createRedisClient(PRIMARY_REDIS_URL!);
|
primaryClient = await createRedisClient(PRIMARY_REDIS_URL!);
|
||||||
return primaryClient;
|
return primaryClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write data to all available Redis instances
|
* Get all available replicas that are not the active client
|
||||||
*/
|
*/
|
||||||
async function writeToAllRedis(
|
async function getAvailableReplicas(activeClientUrl: string): Promise<Redis[]> {
|
||||||
operation: (redis: Redis) => Promise<any>
|
const availableReplicas: Redis[] = [];
|
||||||
): Promise<void> {
|
|
||||||
const clients = [
|
|
||||||
primaryClient,
|
|
||||||
...Array.from(replicaClients.values()),
|
|
||||||
].filter(Boolean) as Redis[];
|
|
||||||
|
|
||||||
const errors: Error[] = [];
|
// Initialize clients if not already done
|
||||||
|
await initializeAllClients();
|
||||||
|
|
||||||
for (const client of clients) {
|
// Check each replica
|
||||||
|
for (const [url, client] of replicaClients.entries()) {
|
||||||
|
if (url !== activeClientUrl && client.status === "ready") {
|
||||||
try {
|
try {
|
||||||
if (client.status === "ready") {
|
const isHealthy = await checkConnection(client, getRedisName(url));
|
||||||
await operation(client);
|
if (isHealthy) {
|
||||||
|
availableReplicas.push(client);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error checking replica ${getRedisName(url)}:`, error);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
errors.push(err as Error);
|
|
||||||
console.error(
|
|
||||||
`Write failed for ${getRedisName(client.options.host || "")}:`,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.length === clients.length && clients.length > 0) {
|
console.log(`Found ${availableReplicas.length} available replicas for replication`);
|
||||||
throw new Error("All Redis writes failed");
|
return availableReplicas;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately replicate a Redis operation to all available replicas
|
||||||
|
*/
|
||||||
|
async function replicateOperation(
|
||||||
|
activeClientUrl: string,
|
||||||
|
key: string,
|
||||||
|
value: string,
|
||||||
|
ttl?: number
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Get available replicas that are not the active client
|
||||||
|
const replicas = await getAvailableReplicas(activeClientUrl);
|
||||||
|
|
||||||
|
if (replicas.length === 0) {
|
||||||
|
console.log('No available replicas for replication');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Replicating operation for key ${key} to ${replicas.length} replicas`);
|
||||||
|
|
||||||
|
// Replicate to each available replica
|
||||||
|
const replicationPromises = replicas.map(async (replica) => {
|
||||||
|
try {
|
||||||
|
if (ttl) {
|
||||||
|
await replica.set(key, value, 'EX', ttl);
|
||||||
|
} else {
|
||||||
|
await replica.set(key, value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Replication failed for key ${key}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(replicationPromises);
|
||||||
|
const successful = results.filter(r => r.status === 'fulfilled' && r.value === true).length;
|
||||||
|
|
||||||
|
console.log(`Successfully replicated to ${successful}/${replicas.length} replicas`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during replication:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a feed mapping in Redis
|
* Synchronize data from a replica to the primary
|
||||||
|
* Used when primary recovers after being down
|
||||||
|
*/
|
||||||
|
async function syncFromReplicaToPrimary(replicaUrl: string): Promise<void> {
|
||||||
|
if (!primaryClient || primaryClient.status !== "ready" || !replicaClients.has(replicaUrl)) {
|
||||||
|
console.log("Cannot sync: primary or replica not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replicaClient = replicaClients.get(replicaUrl)!;
|
||||||
|
console.log(`Starting sync from ${getRedisName(replicaUrl)} to Primary...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all feed keys using scan to prevent blocking
|
||||||
|
const feedKeys: string[] = [];
|
||||||
|
let cursor = '0';
|
||||||
|
|
||||||
|
do {
|
||||||
|
const [nextCursor, keys] = await replicaClient.scan(
|
||||||
|
cursor,
|
||||||
|
'MATCH',
|
||||||
|
'[0-9a-f]*', // Match feed IDs (hex format)
|
||||||
|
'COUNT',
|
||||||
|
'100'
|
||||||
|
);
|
||||||
|
|
||||||
|
cursor = nextCursor;
|
||||||
|
feedKeys.push(...keys);
|
||||||
|
} while (cursor !== '0');
|
||||||
|
|
||||||
|
// Also try scanning for any key pattern that might be a feed ID
|
||||||
|
cursor = '0';
|
||||||
|
const additionalKeys: string[] = [];
|
||||||
|
|
||||||
|
do {
|
||||||
|
const [nextCursor, keys] = await replicaClient.scan(
|
||||||
|
cursor,
|
||||||
|
'MATCH',
|
||||||
|
'feed:*', // Try another pattern
|
||||||
|
'COUNT',
|
||||||
|
'100'
|
||||||
|
);
|
||||||
|
|
||||||
|
cursor = nextCursor;
|
||||||
|
additionalKeys.push(...keys);
|
||||||
|
} while (cursor !== '0');
|
||||||
|
|
||||||
|
feedKeys.push(...additionalKeys);
|
||||||
|
|
||||||
|
// Get chapter cache keys
|
||||||
|
const chapterKeys: string[] = [];
|
||||||
|
cursor = '0';
|
||||||
|
|
||||||
|
do {
|
||||||
|
const [nextCursor, keys] = await replicaClient.scan(
|
||||||
|
cursor,
|
||||||
|
'MATCH',
|
||||||
|
'chapters:*',
|
||||||
|
'COUNT',
|
||||||
|
'100'
|
||||||
|
);
|
||||||
|
|
||||||
|
cursor = nextCursor;
|
||||||
|
chapterKeys.push(...keys);
|
||||||
|
} while (cursor !== '0');
|
||||||
|
|
||||||
|
console.log(`Found ${feedKeys.length} feed keys and ${chapterKeys.length} chapter keys to sync`);
|
||||||
|
|
||||||
|
// Sync feed mappings
|
||||||
|
if (feedKeys.length > 0) {
|
||||||
|
const feedPipeline = primaryClient.pipeline();
|
||||||
|
|
||||||
|
for (const key of feedKeys) {
|
||||||
|
const value = await replicaClient.get(key);
|
||||||
|
const ttl = await replicaClient.ttl(key);
|
||||||
|
|
||||||
|
if (value && ttl > 0) {
|
||||||
|
feedPipeline.set(key, value, 'EX', ttl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await feedPipeline.exec();
|
||||||
|
console.log(`Synced ${feedKeys.length} feed mappings from replica to primary`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync chapter cache
|
||||||
|
if (chapterKeys.length > 0) {
|
||||||
|
const chapterPipeline = primaryClient.pipeline();
|
||||||
|
|
||||||
|
for (const key of chapterKeys) {
|
||||||
|
const value = await replicaClient.get(key);
|
||||||
|
const ttl = await replicaClient.ttl(key);
|
||||||
|
|
||||||
|
if (value && ttl > 0) {
|
||||||
|
chapterPipeline.set(key, value, 'EX', ttl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await chapterPipeline.exec();
|
||||||
|
console.log(`Synced ${chapterKeys.length} chapter caches from replica to primary`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUsedReplica = null; // Reset after successful sync
|
||||||
|
console.log("Replica to primary sync completed successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during replica to primary sync:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a feed mapping in Redis with immediate replication
|
||||||
*/
|
*/
|
||||||
export async function storeFeedMapping(
|
export async function storeFeedMapping(
|
||||||
feedId: string,
|
feedId: string,
|
||||||
|
|
@ -185,31 +452,54 @@ export async function storeFeedMapping(
|
||||||
created_at: new Date().toISOString()
|
created_at: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
await writeToAllRedis(async (redis) =>
|
const stringValue = JSON.stringify(mapping);
|
||||||
redis.set(feedId, JSON.stringify(mapping), "EX", DEFAULT_CACHE_TTL)
|
|
||||||
);
|
try {
|
||||||
|
// First write to primary/active client
|
||||||
|
const redis = await getRedisClient(); // This already handles failover
|
||||||
|
const activeUrl = redis.options.host || '';
|
||||||
|
|
||||||
|
await redis.set(feedId, stringValue, "EX", DEFAULT_CACHE_TTL);
|
||||||
|
console.log(`Successfully stored mapping for feed ${feedId}`);
|
||||||
|
|
||||||
|
// Immediately replicate to other instances
|
||||||
|
await replicateOperation(activeUrl, feedId, stringValue, DEFAULT_CACHE_TTL);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error storing feed mapping for ${feedId}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve a feed mapping from Redis
|
* Retrieve a feed mapping from Redis with automatic failover
|
||||||
*/
|
*/
|
||||||
export async function getFeedMapping(
|
export async function getFeedMapping(
|
||||||
feedId: string
|
feedId: string
|
||||||
): Promise<FeedMapping | null> {
|
): Promise<FeedMapping | null> {
|
||||||
try {
|
try {
|
||||||
const redis = await getRedisClient();
|
const redis = await getRedisClient(); // This already handles failover
|
||||||
const mapping = await redis.get(feedId);
|
const mapping = await redis.get(feedId);
|
||||||
|
|
||||||
if (!mapping) return null;
|
if (!mapping) return null;
|
||||||
|
|
||||||
// Reset expiration on access
|
// Reset expiration on access
|
||||||
|
const activeUrl = redis.options.host || '';
|
||||||
await redis.expire(feedId, DEFAULT_CACHE_TTL);
|
await redis.expire(feedId, DEFAULT_CACHE_TTL);
|
||||||
|
|
||||||
|
// Also reset TTL on other replicas
|
||||||
|
replicateOperation(activeUrl, feedId, mapping, DEFAULT_CACHE_TTL)
|
||||||
|
.catch(err => console.error(`Failed to reset TTL on replicas for ${feedId}:`, err));
|
||||||
|
|
||||||
|
try {
|
||||||
const data = JSON.parse(mapping);
|
const data = JSON.parse(mapping);
|
||||||
return {
|
return {
|
||||||
slugs: data.slugs,
|
slugs: data.slugs,
|
||||||
lang: data.lang
|
lang: data.lang
|
||||||
};
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error parsing feed mapping for ${feedId}:`, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error retrieving feed mapping for ${feedId}:`, error);
|
console.error(`Error retrieving feed mapping for ${feedId}:`, error);
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -227,7 +517,7 @@ export async function warmChapterCache(
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(`Warming chapter cache for ${slugs.length} comics in ${lang}`);
|
console.log(`Warming chapter cache for ${slugs.length} comics in ${lang}`);
|
||||||
const redis = await getRedisClient();
|
const redis = await getRedisClient(); // This already handles failover
|
||||||
|
|
||||||
// Validate slugs
|
// Validate slugs
|
||||||
const validSlugs = slugs.filter(slug =>
|
const validSlugs = slugs.filter(slug =>
|
||||||
|
|
@ -281,5 +571,12 @@ export async function warmChapterCache(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize connections lazily
|
// Initialize connections eagerly to establish connections as soon as possible
|
||||||
|
if (!import.meta.env.SSR) {
|
||||||
|
initializeAllClients()
|
||||||
|
.then(() => console.log("Redis clients initialized"))
|
||||||
|
.catch(err => console.error("Failed to initialize Redis clients:", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize connections lazily as backup
|
||||||
console.log("Redis will connect on first use");
|
console.log("Redis will connect on first use");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue