Fullstack & Cloud Architecture Kiến trúc Fullstack & Cloud · · 14 min read

OpenStock Architecture: Resilient Serverless Event Bus & Multi-Provider AI Fallback Kiến Trúc OpenStock: Event Bus Không Máy Chủ & Cơ Chế Dự Phòng AI Đa Nhà Cung Cấp

A deep dive into OpenStock: dissecting its serverless financial pipeline, Inngest step memoization for stock alerts, and zero-downtime multi-LLM failover. Phân tích chuyên sâu OpenStock: bóc tách kiến trúc xử lý tài chính serverless, kỹ thuật memoization step với Inngest cho cảnh báo giá và cơ chế chuyển đổi AI dự phòng.

Written by Nguyen Cong Ben Nguyễn Công Ben
OpenStock Architecture Banner

Introduction: The Democratization of Financial Data Platforms

Retail investors and modern engineering teams often hit a steep barrier when building or accessing financial intelligence tools: institutional terminal solutions (such as Bloomberg Professional or FactSet) demand tens of thousands of dollars annually, while lightweight consumer apps often lock core analytics behind opaque paywalls.

OpenStock (Open-Dev-Society/OpenStock), an open-source financial monitoring and market intelligence platform, offers an enterprise-grade architectural alternative. Built on Next.js 15, TypeScript, Inngest, MongoDB, and Finnhub, OpenStock addresses three critical engineering hurdles common to fintech applications:

  1. Eliminating Stateful Daemon Overhead: Instead of maintaining long-running worker processes (such as Celery or BullMQ clusters) that incur baseline hosting costs during market closed hours, OpenStock coordinates recurring market polling through deterministic, event-driven serverless orchestration.
  2. Durable Multi-Step Job Execution: Heavy background workflows—such as querying quote APIs for hundreds of alerts, calculating trigger bounds, and dispatching multi-channel notifications—are broken down into idempotent execution steps that withstand serverless function timeouts.
  3. Resilient AI Synthesis: Generative financial digests and personalized investor onboarding are protected against upstream model rate limits and outages via an automated, multi-provider failover engine across Google Gemini, MiniMax, and Siray.ai.

In this deep dive, we inspect the actual OpenStock codebase to understand its architectural mechanics, runtime invariants, and production trade-offs.


1. High-Level System Architecture

OpenStock separates client presentation, financial widget hydration, state persistence, and distributed task orchestration into decoupled subsystems:

flowchart TD
    Client[Next.js 15 App Router Frontend] -->|Direct Script Injection| TV[TradingView Embedded Widgets]
    Client -->|Server Actions / Next Cookies| Auth[Better Auth + MongoDB]
    Client -->|Cached Data Ingestion| Finnhub[Finnhub REST API v1]
    
    subgraph "Distributed Event Plane (Inngest)"
        EventBus[Inngest Event Engine]
        Cron[Cron Triggers: */5min & Weekly]
        
        EventBus --> Step1[Step: Fetch Active Alerts & Dedupe]
        Step1 --> Step2[Step: Batch Quote Polling]
        Step2 --> Step3[Step: Evaluate Thresholds & State Transition]
    end

    subgraph "AI Inference Pipeline"
        Primary[Primary: Google Gemini REST]
        Fallback[Fallback: MiniMax / Siray.ai]
        Primary -.->|Timeout / 4xx / 5xx| Fallback
    end

    Cron --> EventBus
    Step3 --> Broadcast[Notification Dispatch / Kit Newsletter]
    EventBus --> Primary

Core System Components:

  • Presentation & Rendering Engine: Built on Next.js 15 (React 19) and Tailwind CSS v4, combining server components for profile hydration with client-side lifecycle wrappers for heavy TradingView canvas widgets.
  • Identity & Session Management: Utilizes better-auth paired with a direct MongoDB native driver adapter, decoupling session token verification from heavy ORM models.
  • Serverless Event Bus (lib/inngest): Orchestrates asynchronous cron jobs, user lifecycle events, and price alert evaluation pipelines without requiring standalone Node.js daemon servers.
  • Provider Resilience Bridge (lib/ai-provider.ts): Abstracts LLM inference behind a single interface with automatic failover fallback handling.

2. Deep Dive: Inngest Step Memoization for Market Alerts

A common pitfall in serverless architectures is running monolithic cron jobs. If a serverless function polls 50 stock quotes and crashes at quote 48 due to a transient network timeout or rate limit, the entire execution aborts, and retrying the function duplicates alerts for quotes 1 through 47.

OpenStock solves this with durable execution steps in lib/inngest/functions.ts:

export const checkStockAlerts = inngest.createFunction(
    { id: 'check-stock-alerts', triggers: [{ cron: '*/5 * * * *' }] }, // Every 5 minutes
    async ({ step }) => {
        // Step 1: Query active, unexpired alerts
        const activeAlerts = await step.run('fetch-active-alerts', async () => {
            const { connectToDatabase } = await import("@/database/mongoose");
            const { Alert } = await import("@/database/models/alert.model");

            await connectToDatabase();
            const now = new Date();

            return await Alert.find({
                active: true,
                triggered: false,
                expiresAt: { $gt: now }
            }).lean();
        });

        if (!activeAlerts || activeAlerts.length === 0) {
            return { message: 'No active alerts to check.' };
        }

        // Step 2: Deduplicate tickers across all users
        const symbols = [...new Set(activeAlerts.map((a: any) => a.symbol))];

        // Step 3: Fetch batch quotes with symbol deduplication
        const prices = await step.run('fetch-prices', async () => {
            const { getQuote } = await import("@/lib/actions/finnhub.actions");
            const priceMap: Record<string, number> = {};

            for (const sym of symbols) {
                try {
                    const quote = await getQuote(sym as string);
                    if (quote && quote.c) {
                        priceMap[sym as string] = quote.c;
                    }
                } catch (e) {
                    console.error(`Failed to fetch price for ${sym}`, e);
                }
            }
            return priceMap;
        });

        // Step 4: Evaluate triggers in memory
        type TriggeredAlert = { alert: any; currentPrice: number };
        const triggeredAlerts: TriggeredAlert[] = [];

        for (const alert of activeAlerts as any[]) {
            const currentPrice = prices[alert.symbol];
            if (!currentPrice) continue;

            let isTriggered = false;
            if (alert.condition === 'ABOVE' && currentPrice >= alert.targetPrice) {
                isTriggered = true;
            } else if (alert.condition === 'BELOW' && currentPrice <= alert.targetPrice) {
                isTriggered = true;
            }

            if (isTriggered) {
                triggeredAlerts.push({ alert, currentPrice });
            }
        }

        // Step 5: Atomic state transition
        if (triggeredAlerts.length > 0) {
            await step.run('process-triggered-alerts', async () => {
                const { connectToDatabase } = await import("@/database/mongoose");
                const { Alert } = await import("@/database/models/alert.model");
                await connectToDatabase();

                for (const { alert, currentPrice } of triggeredAlerts) {
                    await Alert.findByIdAndUpdate(alert._id, { 
                        triggered: true, 
                        active: false 
                    });
                }
            });
        }

        return { processed: activeAlerts.length, triggered: triggeredAlerts.length };
    }
);

Architectural Benefits:

  1. Ticker Deduplication: If 200 users monitor NVDA at different thresholds, symbols = [...new Set(...)] compresses 200 checks into 1 single HTTP request against Finnhub, preventing instant quota exhaustion on free tiers.
  2. Step Memoization: Inngest serializes the return value of each step.run invocation. If Step 5 fails to write to MongoDB, Inngest resumes execution directly at Step 5 without re-fetching quotes from Finnhub or querying the active alerts list again.

3. Zero-Downtime Multi-Provider AI Fallback Engine

Financial digest generation cannot tolerate downtime during major market opening bells. In lib/ai-provider.ts, OpenStock designs an abstraction layer over heterogeneous LLM backends:

export type AIProviderName = "gemini" | "minimax" | "siray";

export async function callAIProviderWithFallback(
  prompt: string
): Promise<string> {
  const primaryName = (process.env.AI_PROVIDER as AIProviderName) || "gemini";
  const fallbackName = getFallbackProviderName(primaryName);

  try {
    return await callAIProvider(prompt, primaryName);
  } catch (primaryError) {
    console.error(
      `⚠️ ${primaryName} failed, switching to ${fallbackName} fallback`,
      primaryError
    );
    return await callAIProvider(prompt, fallbackName);
  }
}
function getFallbackProviderName(primary: AIProviderName): AIProviderName {
  if (primary === "gemini") {
    if (process.env.MINIMAX_API_KEY) return "minimax";
    if (process.env.SIRAY_API_KEY) return "siray";
    return "minimax";
  }
  return "gemini";
}

The system translates between Google’s native REST payload format (contents[].parts[].text) and standard OpenAI-compatible completions endpoints (choices[0].message.content). When Google Gemini experiences regional latency or quota limits, the orchestrator redirects the context payload to MiniMax or Siray without user intervention.


4. Technical Comparison: OpenStock vs Conventional Architecture

DimensionNaive Monolithic Fintech AppOpenStock Architecture
Worker ModelLong-running Node.js process / Redis BullMQ clusterServerless Inngest event functions (zero idle compute)
Quote Fetching$O(N)$ HTTP requests per registered alert$O(U)$ requests grouped by unique symbol set
Failure RecoveryRetry restarts from step 0; risk of duplicate emailsDeterministic step memoization; resumes at point of failure
Chart RenderingHeavy canvas libraries bundled into client bundleClient-side lazy script injection via TradingView CDN
AI AvailabilityHard dependency on single LLM vendor API keyResilient multi-provider fallback (Gemini → MiniMax / Siray)
Cache StrategyIn-memory Redis cache requiring server upkeepNext.js Server Actions with tagged revalidation (force-cache)

5. Client Lifecycle: Safe TradingView Widget Mounts

Embedding complex third-party analytical widgets into modern React 19 / Next.js client trees frequently results in memory leaks, dual mounting during hot reloads, or broken viewport re-renders.

OpenStock resolves this through a dedicated custom hook (hooks/useTradingViewWidget.tsx):

const useTradingViewWidget = (
    scriptUrl: string, 
    config: Record<string, unknown>, 
    height: number | string = 600
) => {
    const containerRef = useRef<HTMLDivElement | null>(null);

    useEffect(() => {
        if (!containerRef.current) return;

        // 1. Clean up any previous iframe or script nodes
        containerRef.current.innerHTML = '';

        // 2. Prepare container dimensions
        const isAutosize = config.autosize === true;
        const styleHeight = isAutosize ? '100%' : `${height}px`;

        containerRef.current.innerHTML = 
            `<div class="tradingview-widget-container__widget" style="width: 100%; height: ${styleHeight};"></div>`;

        // 3. Inject vendor script with stringified config payload
        const script = document.createElement("script");
        script.src = scriptUrl;
        script.async = true;
        script.innerHTML = JSON.stringify(config);

        containerRef.current.appendChild(script);

        // 4. Teardown on unmount or symbol switch
        return () => {
            if (containerRef.current) {
                containerRef.current.innerHTML = '';
            }
        };
    }, [scriptUrl, JSON.stringify(config), height]);

    return containerRef;
};

By tearing down the inner HTML container on dependency changes and passing serialized configurations directly to external script nodes, the widget avoids React reconciliation conflicts while ensuring clean memory reclamation upon page navigation.


6. Actionable Takeaways for Distributed Web Engineers

  1. Deduplicate Work Before Polling Third-Party APIs: When evaluating multi-tenant triggers, always aggregate queries by the underlying entity (symbol, assetId, accountId). Transforming $O(\text{alerts})$ into $O(\text{distinct tickers})$ is the single most effective way to avoid API rate limits.
  2. Embrace Step-Level Idempotency: Serverless functions are ephemeral by nature. Wrapping sub-procedures in memoized execution steps allows distributed tasks to survive transient crashes without state pollution.
  3. Decouple Heavy Visualizers from Main Bundles: Rather than bloating application bundles with proprietary charting libraries, use sandboxed client wrappers that dynamically inject remote vendor engines on demand.
  4. Design for Model Heterogeneity: Never pin generative features to a single AI model or provider. Implementing an OpenAI-compatible adapter with automatic fallback ensures business continuity across cloud disruptions.

Giới thiệu: Dân chủ hóa Nền tảng Dữ liệu Tài chính

Các nhà đầu tư cá nhân và kỹ sư phần mềm thường gặp phải rào cản chi phí rất lớn khi xây dựng hoặc tiếp cận các công cụ phân tích thị trường tài chính: các giải pháp thiết bị đầu cuối như Bloomberg Professional hay FactSet tiêu tốn hàng chục nghìn USD mỗi năm, trong khi các ứng dụng thương mại phổ thông lại khóa các tính năng phân tích quan trọng đằng sau bức tường thu phí.

OpenStock (Open-Dev-Society/OpenStock), một nền tảng giám sát thị trường và tổng hợp tin tức tài chính mã nguồn mở, mang đến giải pháp kiến trúc thay thế ở cấp độ doanh nghiệp. Được xây dựng trên Next.js 15, TypeScript, Inngest, MongoDB và Finnhub, OpenStock giải quyết 3 bài toán kỹ thuật cốt lõi thường gặp trong các hệ thống fintech:

  1. Triệt tiêu Chi phí Vận hành Worker Truyền thống: Thay vì phải duy trì các tiến trình daemon chạy liên tục (như cụm Celery hay BullMQ) tốn kém tài nguyên máy chủ ngay cả khi thị trường đóng cửa, OpenStock điều phối các tác vụ định kỳ thông qua kiến trúc serverless hướng sự kiện (event-driven).
  2. Thực thi Bền bỉ Đa bước (Durable Multi-Step Execution): Các luồng công việc phức tạp—như truy vấn giá cho hàng trăm cảnh báo, so sánh ngưỡng điều kiện và phát thông báo—được chia nhỏ thành các bước (steps) có tính bất biến (idempotent), ngăn ngừa nguy cơ timeout trong môi trường serverless.
  3. Tổng hợp AI Khả năng Chịu lỗi Cao: Các bản tin tóm tắt thị trường hàng tuần và thư chào mừng cá nhân hóa được bảo vệ khỏi tình trạng quá tải rate limit hoặc gián đoạn dịch vụ thông qua cơ chế chuyển đổi dự phòng (failover) giữa Google Gemini, MiniMax và Siray.ai.

Trong bài phân tích này, chúng ta sẽ đào sâu vào cấu trúc mã nguồn của OpenStock để khám phá các cơ chế kỹ thuật và quyết định kiến trúc đằng sau dự án.


1. Sơ đồ Kiến trúc Hệ thống

OpenStock tách biệt giao diện người dùng, cơ chế nạp widget tài chính, lưu trữ trạng thái và bộ điều phối tác vụ phân tán thành các tầng độc lập:

flowchart TD
    Client[Next.js 15 App Router Frontend] -->|Tiêm Script Trực tiếp| TV[TradingView Embedded Widgets]
    Client -->|Server Actions / Next Cookies| Auth[Better Auth + MongoDB]
    Client -->|Nạp Dữ liệu Cache| Finnhub[Finnhub REST API v1]
    
    subgraph "Tầng Điều phối Sự kiện Phân tán (Inngest)"
        EventBus[Inngest Event Engine]
        Cron[Cron Triggers: */5 phút & Hàng tuần]
        
        EventBus --> Step1[Bước: Lấy Danh sách Alert Hoạt động]
        Step1 --> Step2[Bước: Gom cụm Mã CK & Quét Giá]
        Step2 --> Step3[Bước: Kiểm tra Ngưỡng & Chuyển Trạng thái]
    end

    subgraph "Pipeline Xử lý AI Dự phòng"
        Primary[Chính: Google Gemini REST]
        Fallback[Dự phòng: MiniMax / Siray.ai]
        Primary -.->|Lỗi / Timeout / Quá tải| Fallback
    end

    Cron --> EventBus
    Step3 --> Broadcast[Phát Thông báo / Kit Newsletter]
    EventBus --> Primary

Các Thành phần Nền tảng:

  • Tầng Render & Giao diện: Sử dụng Next.js 15 (React 19) kết hợp Tailwind CSS v4, phân chia ranh giới rõ ràng giữa Server Components (để nạp dữ liệu hồ sơ công ty nhanh chóng) và Client Components (quản lý vòng đời canvas của TradingView).
  • Xác thực & Quản lý Phiên: Tích hợp better-auth kết nối trực tiếp với MongoDB qua native driver adapter, tách biệt hoàn toàn cơ chế xác thực cookie khỏi các ORM cồng kềnh.
  • Event Bus Serverless (lib/inngest): Quản lý toàn bộ cron job định kỳ và các sự kiện người dùng mà không cần cấu hình cụm máy chủ Redis hoặc background worker chuyên dụng.
  • Cầu nối AI Đa nhà cung cấp (lib/ai-provider.ts): Trừu tượng hóa việc giao tiếp LLM, tự động chuyển đổi sang endpoint tương thích OpenAI khi Gemini gặp sự cố.

2. Bóc tách Kỹ thuật: Cơ chế Memoization Step cho Hệ thống Cảnh báo Giá

Một lỗi nghiêm trọng trong kiến trúc serverless là thực thi các tác vụ cron dạng đơn khối (monolith). Giả sử hàm serverless cần kiểm tra 50 cảnh báo cổ phiếu nhưng bị ngắt kết nối ở mã thứ 48 do mạng chập chờn; toàn bộ hàm sẽ thất bại và khi chạy lại, các mã từ 1 đến 47 sẽ bị kiểm tra hoặc gửi thông báo trùng lặp.

OpenStock khắc phục triệt để bằng kỹ thuật durable execution steps với Inngest trong lib/inngest/functions.ts:

export const checkStockAlerts = inngest.createFunction(
    { id: 'check-stock-alerts', triggers: [{ cron: '*/5 * * * *' }] }, // Chạy mỗi 5 phút
    async ({ step }) => {
        // Bước 1: Truy vấn các alert còn hạn và đang hoạt động
        const activeAlerts = await step.run('fetch-active-alerts', async () => {
            const { connectToDatabase } = await import("@/database/mongoose");
            const { Alert } = await import("@/database/models/alert.model");

            await connectToDatabase();
            const now = new Date();

            return await Alert.find({
                active: true,
                triggered: false,
                expiresAt: { $gt: now }
            }).lean();
        });

        if (!activeAlerts || activeAlerts.length === 0) {
            return { message: 'Không có alert nào cần kiểm tra.' };
        }

        // Bước 2: Khử trùng lặp mã cổ phiếu trên toàn bộ user
        const symbols = [...new Set(activeAlerts.map((a: any) => a.symbol))];

        // Bước 3: Lấy giá hàng loạt theo tập hợp mã duy nhất
        const prices = await step.run('fetch-prices', async () => {
            const { getQuote } = await import("@/lib/actions/finnhub.actions");
            const priceMap: Record<string, number> = {};

            for (const sym of symbols) {
                try {
                    const quote = await getQuote(sym as string);
                    if (quote && quote.c) {
                        priceMap[sym as string] = quote.c;
                    }
                } catch (e) {
                    console.error(`Lỗi khi lấy giá cho ${sym}`, e);
                }
            }
            return priceMap;
        });

        // Bước 4: So sánh điều kiện trên bộ nhớ RAM
        type TriggeredAlert = { alert: any; currentPrice: number };
        const triggeredAlerts: TriggeredAlert[] = [];

        for (const alert of activeAlerts as any[]) {
            const currentPrice = prices[alert.symbol];
            if (!currentPrice) continue;

            let isTriggered = false;
            if (alert.condition === 'ABOVE' && currentPrice >= alert.targetPrice) {
                isTriggered = true;
            } else if (alert.condition === 'BELOW' && currentPrice <= alert.targetPrice) {
                isTriggered = true;
            }

            if (isTriggered) {
                triggeredAlerts.push({ alert, currentPrice });
            }
        }

        // Bước 5: Cập nhật trạng thái kích hoạt atomic
        if (triggeredAlerts.length > 0) {
            await step.run('process-triggered-alerts', async () => {
                const { connectToDatabase } = await import("@/database/mongoose");
                const { Alert } = await import("@/database/models/alert.model");
                await connectToDatabase();

                for (const { alert, currentPrice } of triggeredAlerts) {
                    await Alert.findByIdAndUpdate(alert._id, { 
                        triggered: true, 
                        active: false 
                    });
                }
            });
        }

        return { processed: activeAlerts.length, triggered: triggeredAlerts.length };
    }
);

Lợi ích Kiến trúc:

  1. Khử trùng lặp Ticker: Nếu có 200 nhà đầu tư cùng đặt cảnh báo cho mã NVDA ở các mức giá khác nhau, dòng lệnh symbols = [...new Set(...)] gộp toàn bộ thành 1 request HTTP duy nhất tới Finnhub, bảo toàn hạn ngạch 60 request/phút của gói miễn phí.
  2. Lưu trữ Trạng thái Từng Bước (Memoization): Inngest tự động lưu kết quả trả về của từng step.run. Nếu Bước 5 gặp sự cố khi ghi cơ sở dữ liệu MongoDB, Inngest sẽ tự thử lại từ Bước 5 mà không cần gọi lại API Finnhub hay truy vấn lại danh sách active alerts.

3. Kiến trúc AI Dự phòng Đảm bảo Hoạt động Liên tục (Zero Downtime)

Việc tổng hợp tin tức tài chính trước giờ mở cửa phiên giao dịch không được phép trễ hạn do sự cố API bên thứ ba. Trong tệp lib/ai-provider.ts, OpenStock xây dựng một lớp adapter chuyển mạch thông minh:

export type AIProviderName = "gemini" | "minimax" | "siray";

export async function callAIProviderWithFallback(
  prompt: string
): Promise<string> {
  const primaryName = (process.env.AI_PROVIDER as AIProviderName) || "gemini";
  const fallbackName = getFallbackProviderName(primaryName);

  try {
    return await callAIProvider(prompt, primaryName);
  } catch (primaryError) {
    console.error(
      `⚠️ Nhà cung cấp chính ${primaryName} gặp lỗi, chuyển sang dự phòng ${fallbackName}`,
      primaryError
    );
    return await callAIProvider(prompt, fallbackName);
  }
}
function getFallbackProviderName(primary: AIProviderName): AIProviderName {
  if (primary === "gemini") {
    if (process.env.MINIMAX_API_KEY) return "minimax";
    if (process.env.SIRAY_API_KEY) return "siray";
    return "minimax";
  }
  return "gemini";
}

Hệ thống tự động chuẩn hóa sự khác biệt giữa cấu trúc REST thuần của Google Gemini (contents[].parts[].text) và cấu trúc chuẩn tương thích OpenAI của MiniMax/Siray (choices[0].message.content). Khi Gemini chạm hạn ngạch (429) hoặc phản hồi chậm, luồng xử lý lập tức được định tuyến lại mà không gây lỗi cho người dùng cuối.


4. Bảng So sánh Kỹ thuật: OpenStock và Mô hình Truyền thống

Tiêu chíỨng dụng Tài chính Monolithic Truyền thốngKiến trúc Hiện đại của OpenStock
Mô hình WorkerDuy trì tiến trình Node.js / Cụm BullMQ với Redis liên tụcHàm Inngest Serverless theo sự kiện (chi phí tính toán nhàn rỗi = 0)
Truy vấn Giá$O(N)$ HTTP requests tương ứng mỗi alert đăng ký$O(U)$ requests nhóm theo tập hợp ticker duy nhất
Khắc phục Sự cốChạy lại từ đầu (step 0); rủi ro gửi thông báo lặpMemoization từng step; khôi phục chính xác tại điểm đứt gãy
Hiển thị Biểu đồTải thư viện biểu đồ nặng trực tiếp vào bundleTiêm script động từ CDN TradingView qua Client Hook
Độ ổn định AIPhụ thuộc hoàn toàn vào một API key LLM duy nhấtCơ chế Fallback tự động đa nhà cung cấp (Gemini → MiniMax / Siray)
Cơ chế CacheCụm Redis riêng biệt cần quản trị hạ tầngNext.js Server Actions với cơ chế revalidation theo thẻ (force-cache)

5. Quản lý Vòng đời Client: Gắn kết An toàn Widget TradingView

Việc nhúng các widget phân tích phức tạp của bên thứ ba vào cây DOM trong môi trường React 19 / Next.js thường gây ra rò rỉ bộ nhớ (memory leak), nhân đôi widget khi hot-reload hoặc vỡ giao diện khi thay đổi kích thước màn hình.

OpenStock xử lý vấn đề này thông qua một hook chuyên dụng (hooks/useTradingViewWidget.tsx):

const useTradingViewWidget = (
    scriptUrl: string, 
    config: Record<string, unknown>, 
    height: number | string = 600
) => {
    const containerRef = useRef<HTMLDivElement | null>(null);

    useEffect(() => {
        if (!containerRef.current) return;

        // 1. Dọn dẹp iframe và script cũ trước khi khởi tạo
        containerRef.current.innerHTML = '';

        // 2. Thiết lập khung chứa với kích thước động
        const isAutosize = config.autosize === true;
        const styleHeight = isAutosize ? '100%' : `${height}px`;

        containerRef.current.innerHTML = 
            `<div class="tradingview-widget-container__widget" style="width: 100%; height: ${styleHeight};"></div>`;

        // 3. Tiêm thẻ script của TradingView với config dạng chuỗi JSON
        const script = document.createElement("script");
        script.src = scriptUrl;
        script.async = true;
        script.innerHTML = JSON.stringify(config);

        containerRef.current.appendChild(script);

        // 4. Giải phóng tài nguyên khi component unmount
        return () => {
            if (containerRef.current) {
                containerRef.current.innerHTML = '';
            }
        };
    }, [scriptUrl, JSON.stringify(config), height]);

    return containerRef;
};

Bằng cách xóa sạch DOM cũ trong hàm cleanup và serialize cấu hình trực tiếp vào thẻ script, giải pháp này ngăn ngừa xung đột với thuật toán đối soát (reconciliation) của React, đồng thời giải phóng bộ nhớ khi người dùng điều hướng sang trang khác.


6. Bài học Kinh nghiệm dành cho Kỹ sư Phát triển Hệ thống

  1. Khử trùng lặp Dữ liệu trước khi Gọi API Bên ngoài: Khi thiết kế các hệ thống giám sát đa người dùng, luôn gom nhóm tác vụ theo thực thể cơ sở (symbol, assetId, accountId). Việc chuyển đổi độ phức tạp từ $O(\text{alerts})$ về $O(\text{distinct tickers})$ là giải pháp tối ưu nhất để tránh vượt ngưỡng rate limit.
  2. Thiết kế Tác vụ Đảm bảo Tính Bất biến (Idempotent Steps): Bản chất của môi trường serverless là tính tạm thời (ephemeral). Chia nhỏ luồng nghiệp vụ thành các bước thực thi độc lập và có khả năng memoize sẽ giúp hệ thống sống sót qua các lỗi mạng tức thời mà không làm biến dạng dữ liệu.
  3. Tách biệt Thư viện Nặng khỏi Client Bundle: Không nên đóng gói trực tiếp các engine biểu đồ phức tạp vào mã nguồn ứng dụng. Hãy tận dụng CDN của nhà cung cấp và quản lý việc tiêm script một cách chặt chẽ qua lifecycle hooks.
  4. Chuẩn bị Kịch bản Dự phòng cho Tầng Sinh AI: Không bao giờ phụ thuộc tuyệt đối vào một nhà cung cấp LLM duy nhất. Việc chuẩn hóa adapter theo định dạng OpenAI và thiết lập cơ chế chuyển mạch tự động sẽ đảm bảo tính liên tục của dịch vụ khi có sự cố diện rộng.