cloudflare下排除无用nodejs包
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/* Cloudflare build shim: outbound proxy agents are not supported by Workers. */
|
||||
|
||||
export class HttpsProxyAgent {
|
||||
constructor(..._args: unknown[]) {
|
||||
throw new Error('HttpsProxyAgent is not supported in Cloudflare Workers');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/* Cloudflare build shim: Workers provide standards-based fetch globally. */
|
||||
|
||||
export default function nodeFetch(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit
|
||||
): Promise<Response> {
|
||||
return fetch(input, init);
|
||||
}
|
||||
|
||||
export const Headers = globalThis.Headers;
|
||||
export const Request = globalThis.Request;
|
||||
export const Response = globalThis.Response;
|
||||
@@ -0,0 +1,57 @@
|
||||
/* Cloudflare build shim for Node-only packages.
|
||||
*
|
||||
* This keeps unsupported Node dependencies out of the Worker bundle while
|
||||
* failing loudly if a Node-only code path is actually executed on Workers.
|
||||
*/
|
||||
|
||||
function unsupported(name = 'This Node-only package'): never {
|
||||
throw new Error(`${name} is not supported in Cloudflare Workers`);
|
||||
}
|
||||
|
||||
export function createClient(): never {
|
||||
return unsupported('redis');
|
||||
}
|
||||
|
||||
export class Server {
|
||||
constructor() {
|
||||
unsupported('socket.io');
|
||||
}
|
||||
}
|
||||
|
||||
export class Socket {
|
||||
constructor() {
|
||||
unsupported('socket.io');
|
||||
}
|
||||
}
|
||||
|
||||
export class Pool {
|
||||
constructor() {
|
||||
unsupported('pg');
|
||||
}
|
||||
}
|
||||
|
||||
export const sql = new Proxy(() => undefined, {
|
||||
apply: () => unsupported('@vercel/postgres'),
|
||||
get: () => unsupported('@vercel/postgres'),
|
||||
});
|
||||
|
||||
const shim: any = new Proxy(
|
||||
function nodeUnsupportedDefault() {
|
||||
unsupported();
|
||||
},
|
||||
{
|
||||
apply: () => unsupported(),
|
||||
construct: () => unsupported(),
|
||||
get: (_target, prop) => {
|
||||
if (prop === 'createTransport') {
|
||||
return () => unsupported('nodemailer');
|
||||
}
|
||||
if (prop === 'default') {
|
||||
return shim;
|
||||
}
|
||||
return () => unsupported(String(prop));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default shim;
|
||||
+15
-3
@@ -2,7 +2,6 @@
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { MusicPlayRecord } from './db.client';
|
||||
import { KvrocksStorage } from './kvrocks.db';
|
||||
import { MangaReadRecord, MangaShelfItem } from './manga.types';
|
||||
import { BookReadRecord, BookShelfItem } from './book.types';
|
||||
import {
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
MusicV2PlaylistItem,
|
||||
MusicV2PlaylistRecord,
|
||||
} from './music-v2';
|
||||
import { RedisStorage } from './redis.db';
|
||||
import {
|
||||
DanmakuFilterConfig,
|
||||
Favorite,
|
||||
@@ -18,9 +16,10 @@ import {
|
||||
PlayRecord,
|
||||
SkipConfig,
|
||||
} from './types';
|
||||
import { UpstashRedisStorage } from './upstash.db';
|
||||
|
||||
// storage type 常量: 'localstorage' | 'redis' | 'upstash' | 'kvrocks' | 'd1' | 'postgres',默认 'localstorage'
|
||||
const IS_CLOUDFLARE_BUILD =
|
||||
process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare';
|
||||
const STORAGE_TYPE =
|
||||
(process.env.NEXT_PUBLIC_STORAGE_TYPE as
|
||||
| 'localstorage'
|
||||
@@ -35,10 +34,23 @@ const STORAGE_TYPE =
|
||||
function createStorage(): IStorage {
|
||||
switch (STORAGE_TYPE) {
|
||||
case 'redis':
|
||||
if (IS_CLOUDFLARE_BUILD) {
|
||||
throw new Error(
|
||||
'Node Redis storage is not supported in Cloudflare builds. Use D1 or Upstash instead.'
|
||||
);
|
||||
}
|
||||
const { RedisStorage } = require('./redis.db');
|
||||
return new RedisStorage();
|
||||
case 'upstash':
|
||||
const { UpstashRedisStorage } = require('./upstash.db');
|
||||
return new UpstashRedisStorage();
|
||||
case 'kvrocks':
|
||||
if (IS_CLOUDFLARE_BUILD) {
|
||||
throw new Error(
|
||||
'Kvrocks storage is not supported in Cloudflare builds. Use D1 or Upstash instead.'
|
||||
);
|
||||
}
|
||||
const { KvrocksStorage } = require('./kvrocks.db');
|
||||
return new KvrocksStorage();
|
||||
case 'd1':
|
||||
// D1Storage 只能在服务端使用,客户端会报错
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { StandardRedisAdapter } from './redis-adapter';
|
||||
import { BaseRedisStorage, createRedisClient, createRetryWrapper } from './redis-base.db';
|
||||
import { BaseRedisStorage } from './redis-base.db';
|
||||
import { createRedisClient, createRetryWrapper } from './redis-node-client';
|
||||
|
||||
export class KvrocksStorage extends BaseRedisStorage {
|
||||
constructor() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { Redis } from '@upstash/redis';
|
||||
import { RedisClientType } from 'redis';
|
||||
import type { RedisClientType } from 'redis';
|
||||
|
||||
/**
|
||||
* 统一的 Redis 适配器接口
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { MangaReadRecord, MangaShelfItem } from './manga.types';
|
||||
import { BookReadRecord, BookShelfItem } from './book.types';
|
||||
@@ -30,141 +28,6 @@ function ensureStringArray(value: any[]): string[] {
|
||||
// 内存锁:用于防止同一用户的并发播放记录操作(迁移、清理等)
|
||||
const playRecordLocks = new Map<string, Promise<void>>();
|
||||
|
||||
// 连接配置接口
|
||||
export interface RedisConnectionConfig {
|
||||
url: string;
|
||||
clientName: string; // 用于日志显示,如 "Redis" 或 "Pika"
|
||||
}
|
||||
|
||||
// 添加Redis操作重试包装器
|
||||
export function createRetryWrapper(
|
||||
clientName: string,
|
||||
getClient: () => RedisClientType
|
||||
) {
|
||||
return async function withRetry<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries = 3
|
||||
): Promise<T> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (err: any) {
|
||||
const isLastAttempt = i === maxRetries - 1;
|
||||
const isConnectionError =
|
||||
err.message?.includes('Connection') ||
|
||||
err.message?.includes('ECONNREFUSED') ||
|
||||
err.message?.includes('ENOTFOUND') ||
|
||||
err.code === 'ECONNRESET' ||
|
||||
err.code === 'EPIPE';
|
||||
|
||||
if (isConnectionError && !isLastAttempt) {
|
||||
console.log(
|
||||
`${clientName} operation failed, retrying... (${
|
||||
i + 1
|
||||
}/${maxRetries})`
|
||||
);
|
||||
console.error('Error:', err.message);
|
||||
|
||||
// 等待一段时间后重试
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
|
||||
|
||||
// 尝试重新连接
|
||||
try {
|
||||
const client = getClient();
|
||||
if (!client.isOpen) {
|
||||
await client.connect();
|
||||
}
|
||||
} catch (reconnectErr) {
|
||||
console.error('Failed to reconnect:', reconnectErr);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Max retries exceeded');
|
||||
};
|
||||
}
|
||||
|
||||
// 创建客户端的工厂函数
|
||||
export function createRedisClient(
|
||||
config: RedisConnectionConfig,
|
||||
globalSymbol: symbol
|
||||
): RedisClientType {
|
||||
let client: RedisClientType | undefined = (global as any)[globalSymbol];
|
||||
|
||||
if (!client) {
|
||||
if (!config.url) {
|
||||
throw new Error(`${config.clientName}_URL env variable not set`);
|
||||
}
|
||||
|
||||
// 创建客户端配置
|
||||
const clientConfig: any = {
|
||||
url: config.url,
|
||||
socket: {
|
||||
// 重连策略:指数退避,最大30秒
|
||||
reconnectStrategy: (retries: number) => {
|
||||
console.log(
|
||||
`${config.clientName} reconnection attempt ${retries + 1}`
|
||||
);
|
||||
if (retries > 10) {
|
||||
console.error(
|
||||
`${config.clientName} max reconnection attempts exceeded`
|
||||
);
|
||||
return false; // 停止重连
|
||||
}
|
||||
return Math.min(1000 * Math.pow(2, retries), 30000); // 指数退避,最大30秒
|
||||
},
|
||||
connectTimeout: 10000, // 10秒连接超时
|
||||
// 设置no delay,减少延迟
|
||||
noDelay: true,
|
||||
},
|
||||
// 添加其他配置
|
||||
pingInterval: 30000, // 30秒ping一次,保持连接活跃
|
||||
};
|
||||
|
||||
client = createClient(clientConfig);
|
||||
|
||||
// 添加错误事件监听
|
||||
client.on('error', (err) => {
|
||||
console.error(`${config.clientName} client error:`, err);
|
||||
});
|
||||
|
||||
client.on('connect', () => {
|
||||
console.log(`${config.clientName} connected`);
|
||||
});
|
||||
|
||||
client.on('reconnecting', () => {
|
||||
console.log(`${config.clientName} reconnecting...`);
|
||||
});
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log(`${config.clientName} ready`);
|
||||
});
|
||||
|
||||
// 初始连接,带重试机制
|
||||
const connectWithRetry = async () => {
|
||||
try {
|
||||
await client!.connect();
|
||||
console.log(`${config.clientName} connected successfully`);
|
||||
} catch (err) {
|
||||
console.error(`${config.clientName} initial connection failed:`, err);
|
||||
console.log('Will retry in 5 seconds...');
|
||||
setTimeout(connectWithRetry, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
connectWithRetry();
|
||||
|
||||
(global as any)[globalSymbol] = client;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
// 抽象基类,包含所有通用的Redis操作逻辑
|
||||
export abstract class BaseRedisStorage implements IStorage {
|
||||
protected adapter: RedisAdapter;
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
|
||||
// 连接配置接口
|
||||
export interface RedisConnectionConfig {
|
||||
url: string;
|
||||
clientName: string; // 用于日志显示,如 "Redis" 或 "Pika"
|
||||
}
|
||||
|
||||
// 添加 Redis 操作重试包装器。这个文件只给 Node Redis/Kvrocks 使用,
|
||||
// 避免 Cloudflare/D1/Upstash 构建因为 BaseRedisStorage 引入 node-redis。
|
||||
export function createRetryWrapper(
|
||||
clientName: string,
|
||||
getClient: () => RedisClientType
|
||||
) {
|
||||
return async function withRetry<T>(
|
||||
operation: () => Promise<T>,
|
||||
maxRetries = 3
|
||||
): Promise<T> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (err: any) {
|
||||
const isLastAttempt = i === maxRetries - 1;
|
||||
const isConnectionError =
|
||||
err.message?.includes('Connection') ||
|
||||
err.message?.includes('ECONNREFUSED') ||
|
||||
err.message?.includes('ENOTFOUND') ||
|
||||
err.code === 'ECONNRESET' ||
|
||||
err.code === 'EPIPE';
|
||||
|
||||
if (isConnectionError && !isLastAttempt) {
|
||||
console.log(
|
||||
`${clientName} operation failed, retrying... (${
|
||||
i + 1
|
||||
}/${maxRetries})`
|
||||
);
|
||||
console.error('Error:', err.message);
|
||||
|
||||
// 等待一段时间后重试
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
|
||||
|
||||
// 尝试重新连接
|
||||
try {
|
||||
const client = getClient();
|
||||
if (!client.isOpen) {
|
||||
await client.connect();
|
||||
}
|
||||
} catch (reconnectErr) {
|
||||
console.error('Failed to reconnect:', reconnectErr);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Max retries exceeded');
|
||||
};
|
||||
}
|
||||
|
||||
// 创建客户端的工厂函数
|
||||
export function createRedisClient(
|
||||
config: RedisConnectionConfig,
|
||||
globalSymbol: symbol
|
||||
): RedisClientType {
|
||||
let client: RedisClientType | undefined = (global as any)[globalSymbol];
|
||||
|
||||
if (!client) {
|
||||
if (!config.url) {
|
||||
throw new Error(`${config.clientName}_URL env variable not set`);
|
||||
}
|
||||
|
||||
// 创建客户端配置
|
||||
const clientConfig: any = {
|
||||
url: config.url,
|
||||
socket: {
|
||||
// 重连策略:指数退避,最大30秒
|
||||
reconnectStrategy: (retries: number) => {
|
||||
console.log(
|
||||
`${config.clientName} reconnection attempt ${retries + 1}`
|
||||
);
|
||||
if (retries > 10) {
|
||||
console.error(
|
||||
`${config.clientName} max reconnection attempts exceeded`
|
||||
);
|
||||
return false; // 停止重连
|
||||
}
|
||||
return Math.min(1000 * Math.pow(2, retries), 30000); // 指数退避,最大30秒
|
||||
},
|
||||
connectTimeout: 10000, // 10秒连接超时
|
||||
// 设置 no delay,减少延迟
|
||||
noDelay: true,
|
||||
},
|
||||
// 添加其他配置
|
||||
pingInterval: 30000, // 30秒 ping 一次,保持连接活跃
|
||||
};
|
||||
|
||||
client = createClient(clientConfig);
|
||||
|
||||
// 添加错误事件监听
|
||||
client.on('error', (err) => {
|
||||
console.error(`${config.clientName} client error:`, err);
|
||||
});
|
||||
|
||||
client.on('connect', () => {
|
||||
console.log(`${config.clientName} connected`);
|
||||
});
|
||||
|
||||
client.on('reconnecting', () => {
|
||||
console.log(`${config.clientName} reconnecting...`);
|
||||
});
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log(`${config.clientName} ready`);
|
||||
});
|
||||
|
||||
// 初始连接,带重试机制
|
||||
const connectWithRetry = async () => {
|
||||
try {
|
||||
await client!.connect();
|
||||
console.log(`${config.clientName} connected successfully`);
|
||||
} catch (err) {
|
||||
console.error(`${config.clientName} initial connection failed:`, err);
|
||||
console.log('Will retry in 5 seconds...');
|
||||
setTimeout(connectWithRetry, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
connectWithRetry();
|
||||
|
||||
(global as any)[globalSymbol] = client;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { StandardRedisAdapter } from './redis-adapter';
|
||||
import { BaseRedisStorage, createRedisClient, createRetryWrapper } from './redis-base.db';
|
||||
import { BaseRedisStorage } from './redis-base.db';
|
||||
import { createRedisClient, createRetryWrapper } from './redis-node-client';
|
||||
|
||||
export class RedisStorage extends BaseRedisStorage {
|
||||
constructor() {
|
||||
|
||||
Reference in New Issue
Block a user