Tại sao cần patterns riêng để xử lý bất đồng bộ

Async Patterns là các mẫu thiết kế giúp xử lý các tác vụ bất đồng bộ trong JavaScript và TypeScript. Các mẫu này giúp quản lý luồng thực thi, xử lý sự kiện và giao tiếp real-time một cách hiệu quả.

Các pattern này đặc biệt quan trọng trong phát triển web hiện đại, nơi các tác vụ bất đồng bộ như API calls, xử lý sự kiện và real-time updates là phổ biến.

Các Async Pattern phổ biến

1. Promises & Async/Await

Promises và Async/Await là các pattern cơ bản cho việc xử lý tác vụ bất đồng bộ.

Ứng dụng phổ biến:

  • API calls
  • File operations
  • Database queries
  • Tác vụ timeout/interval

2. Reactive Patterns

Reactive Patterns tập trung vào xử lý luồng dữ liệu và sự kiện theo cách reactive.

Ứng dụng phổ biến:

  • Event handling
  • State management
  • Data streams
  • Real-time updates

3. Polling vs WebSockets

So sánh và phân tích hai cách tiếp cận chính cho real-time communication.

Ứng dụng phổ biến:

  • Chat applications
  • Live updates
  • Notifications
  • Real-time dashboards

4. Pub-Sub Pattern

Mô hình publish–subscribe tách publisher khỏi subscriber, phù hợp event bus, message queue nhẹ, và các luồng async không cần biết nhau trực tiếp.

Ứng dụng phổ biến:

  • Event bus trong ứng dụng lớn
  • Thông báo real-time (kết hợp WebSocket/SSE)
  • Giảm coupling giữa module

So sánh các Async Pattern

PatternMục đích chínhKhi nào sử dụng
Promises & Async/AwaitXử lý tác vụ bất đồng bộKhi cần xử lý các tác vụ tuần tự
ReactiveXử lý luồng dữ liệuKhi cần xử lý nhiều sự kiện và updates
Polling/WebSocketsReal-time communicationKhi cần giao tiếp real-time
Pub-SubTách publisher/subscriberNhiều listener, coupling lỏng

Lợi ích của Async Patterns

  1. Hiệu năng tốt hơn

    • Không block main thread
    • Xử lý nhiều tác vụ đồng thời
    • Tối ưu tài nguyên
  2. Code dễ đọc hơn

    • Logic rõ ràng
    • Dễ maintain
    • Dễ debug
  3. Khả năng mở rộng

    • Dễ thêm tính năng mới
    • Dễ scale
    • Linh hoạt
  4. Trải nghiệm người dùng

    • UI responsive
    • Updates real-time
    • Không bị đóng băng

Thách thức khi sử dụng Async Patterns

  1. Độ phức tạp

    • Learning curve cao
    • Khó debug
    • Error handling phức tạp
  2. Memory management

    • Memory leaks
    • Resource cleanup
    • Event listener management
  3. Race conditions

    • Timing issues
    • State synchronization
    • Data consistency
  4. Testing

    • Async test cases
    • Mocking/stubbing
    • Coverage

Best Practices

  1. Error Handling

    async function safeAsync<T>(
      promise: Promise<T>
    ): Promise<[T | null, Error | null]> {
      try {
        const data = await promise;
        return [data, null];
      } catch (error) {
        return [null, error as Error];
      }
    }
    
  2. Cancellation

    const controller = new AbortController();
    const { signal } = controller;
    
    fetch(url, { signal })
      .then((response) => response.json())
      .catch((error) => {
        if (error.name === "AbortError") {
          console.log("Fetch cancelled");
        }
      });
    
    // Cancel the fetch
    controller.abort();
    
  3. Resource Cleanup

    class ResourceManager {
      private cleanupFns: (() => void)[] = [];
    
      addCleanup(fn: () => void): void {
        this.cleanupFns.push(fn);
      }
    
      cleanup(): void {
        this.cleanupFns.forEach((fn) => fn());
        this.cleanupFns = [];
      }
    }
    
  4. Progress Tracking

    interface ProgressCallback {
      (progress: number): void;
    }
    
    async function downloadWithProgress(
      url: string,
      onProgress: ProgressCallback
    ): Promise<Blob> {
      const response = await fetch(url);
      const reader = response.body!.getReader();
      const contentLength = +response.headers.get("Content-Length")!;
    
      let receivedLength = 0;
      const chunks = [];
    
      while (true) {
        const { done, value } = await reader.read();
    
        if (done) break;
    
        chunks.push(value);
        receivedLength += value.length;
        onProgress((receivedLength / contentLength) * 100);
      }
    
      return new Blob(chunks);
    }
    

Khi nào nên sử dụng Async Patterns?

  1. API Interactions

    • HTTP requests
    • GraphQL queries
    • WebSocket connections
    • Server-Sent Events
  2. UI Updates

    • Form handling
    • Data visualization
    • Animation
    • User input
  3. Data Processing

    • File uploads
    • Image processing
    • Data transformation
    • Batch operations
  4. Real-time Features

    • Chat
    • Notifications
    • Live updates
    • Collaborative features

Kết hợp các Pattern

  1. Promises với Reactive (RxJS 7+)

    import { from, of } from "rxjs";
    import { switchMap, retry, catchError } from "rxjs";
    
    from(fetch("/api/data"))
      .pipe(
        switchMap((response) => response.json()),
        retry({ count: 3, delay: 1000 }),
        catchError((error) => of({ error }))
      )
      .subscribe({
        next: (data) => console.log(data),
        error: (err) => console.error(err),
      });
    

    Từ RxJS 7, operator (switchMap, retry, catchError…) import trực tiếp từ "rxjs" thay vì "rxjs/operators". Form subscribe(next, err, complete) cũ vẫn chạy nhưng đã deprecated, ưu tiên object form { next, error, complete }.

  2. WebSocket với Reactive

    import { webSocket } from "rxjs/webSocket";
    import { filter, debounceTime } from "rxjs";
    
    interface Message {
      type: string;
      [key: string]: unknown;
    }
    
    const socket$ = webSocket<Message>("ws://example.com");
    
    socket$
      .pipe(
        filter((msg) => msg.type === "update"),
        debounceTime(300)
      )
      .subscribe({
        next: (msg) => console.log(msg),
        error: (err) => console.error(err),
        complete: () => console.log("Complete"),
      });
    
  3. Async/Await với Polling (có AbortController và backoff đúng)

    async function pollWithBackoff<T>(
      fn: (signal: AbortSignal) => Promise<T>,
      {
        maxAttempts = 5,
        initialDelay = 1000,
        signal,
      }: {
        maxAttempts?: number;
        initialDelay?: number;
        signal?: AbortSignal;
      } = {}
    ): Promise<T> {
      let lastError: unknown;
      for (let attempt = 0; attempt < maxAttempts; attempt++) {
        signal?.throwIfAborted();
        try {
          return await fn(signal ?? new AbortController().signal);
        } catch (error) {
          lastError = error;
          // Exponential backoff: delay × 2^attempt, cap 30s, cộng jitter ngẫu nhiên
          const delay = Math.min(initialDelay * 2 ** attempt, 30_000);
          const jitter = Math.random() * delay * 0.2;
          await new Promise((resolve) => setTimeout(resolve, delay + jitter));
        }
      }
      throw new Error(`Max attempts reached`, { cause: lastError });
    }
    
    // Cách dùng
    const controller = new AbortController();
    setTimeout(() => controller.abort(), 10_000); // timeout toàn cuộc
    
    const data = await pollWithBackoff(
      (signal) => fetch("/api/slow", { signal }).then((r) => r.json()),
      { maxAttempts: 5, signal: controller.signal }
    );
    

    Lưu ý: (1) tăng delay trước retry tiếp theo (attempt 0 → delay×1, attempt 1 → delay×2, …), không phải sau mỗi lần fail; (2) thêm jitter tránh thundering herd; (3) forward AbortSignal xuống tận fetch; (4) dùng Error.cause để giữ lỗi gốc cho debug.

Async Patterns là công cụ thiết yếu trong phát triển ứng dụng web hiện đại. Việc hiểu và áp dụng đúng các pattern này sẽ giúp xây dựng ứng dụng có hiệu năng tốt, dễ bảo trì và mở rộng.

Tuy nhiên, việc lựa chọn pattern phù hợp cần dựa trên yêu cầu cụ thể của dự án, khả năng của team và các ràng buộc về hiệu năng. Không có một pattern nào là tối ưu cho mọi trường hợp, và việc kết hợp các pattern có thể là giải pháp tốt nhất cho nhiều tình huống.