cloudflare下排除无用nodejs包

This commit is contained in:
mtvpls
2026-06-11 11:57:29 +08:00
parent 25b068f673
commit 99bcc8ed77
11 changed files with 264 additions and 151 deletions
@@ -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');
}
}
+12
View File
@@ -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
View File
@@ -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 只能在服务端使用,客户端会报错
+2 -1
View File
@@ -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 -1
View File
@@ -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 适配器接口
-137
View File
@@ -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;
+139
View File
@@ -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
View File
@@ -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() {