Điều phối build qua dependency graph và observer pattern

Trong monorepo, package B import từ package A — A phải build trước B. Khi có hàng chục package với dependency chéo, ai build trước ai build sau là một bài toán topological sort. Cách thường thấy: tính toposort tường minh bằng thuật toán Kahn hoặc DFS, rồi build theo thứ tự. Cách thanh thoát hơn: build dependency graph dưới dạng đồ thị quan sát viên (observer) — leaf package (không depend ai) build trước; mỗi package có dependency tự đăng ký làm observer của dependency đó; khi dependency build xong, package nhận notify và tự kiểm tra “đã hết dependency chưa, nếu rồi thì build”. Không cần scheduler trung tâm, không cần lấy thứ tự rõ ràng — kết quả thứ tự đúng là sản phẩm phụ của event flow.

Cấu trúc graph

flowchart BT
    A["@teko/utils<br/>(leaf)"] --> B["@teko/components"]
    A --> C["@teko/api-client"]
    B --> D["@teko/app-checkout"]
    C --> D
    B --> E["@teko/app-product"]

@teko/utils không depend ai → build trước. @teko/components@teko/api-client đăng ký observer của @teko/utils; khi utils xong, cả hai cùng được notify và tự bắt đầu (chạy song song vì độc lập). @teko/app-checkout đăng ký observer của cả hai — chỉ build khi cả componentsapi-client đều notify.

Cài đặt qua Observable/Observer

Bộ ba primitive đơn giản: Observable lưu danh sách observer và notifyObservers; Observerupdate(observable, arg); Package Observable mà cũng implements Observer.

class Observable {
  constructor() { this.observers = []; }
  addObserver(o) { this.observers.push(o); }
  async notifyObservers(arg) {
    await Promise.all(this.observers.map(o => o.update(this, arg)));
  }
}

class Package extends Observable {
  constructor(root) {
    super();
    this.dependencies = new Set();
  }

  addDependency(dep) {
    this.dependencies.add(dep);
    dep.addObserver(this);   // tôi sẽ được notify khi dep xong
  }

  async update(o) {           // bị notify từ một dep
    this.dependencies.delete(o);
    if (this.dependencies.size === 0) {
      await this.build();
    }
  }

  async build() {
    await this.builder.build();
    await this.notifyObservers();  // báo cho package phụ thuộc tôi
  }
}

Hai dòng then chốt: dep.addObserver(this) ở khi setup graph, và this.dependencies.delete(o) + check size === 0 ở callback. Không có thuật toán toposort tường minh trong code — sự đúng đắn đến từ invariant của graph và monoton giảm của set dependency.

Bootstrap

const leafPackages = packages.filter(pkg => pkg.dependencies.size === 0);
await Promise.all(leafPackages.map(pkg => pkg.build()));

Chỉ cần kick off các leaf — đồ thị tự “lan” qua observer chain. Promise.all của leaves trả về khi cả đồ thị xong (vì mọi root đường đi đều bắt đầu từ leaf, mọi root đường đi đều kết thúc khi không còn observer được trigger).

Vì sao pattern này thanh thoát

So với toposort tường minh + queue:

Trade-off: khó debug khi sai. Stack trace từ một build failure có thể đi qua nhiều notifyObservers async, mờ dần. Toposort tường minh cho stack rõ “đang ở phase N”.

Khi nào không nên dùng pattern này

Trải nghiệm cá nhân

Cài đặt này là phần thanh thoát nhất khi viết teko-kit (Teko, 5/2025). Khi đọc lại, thấy code chính là graph — không có “scheduler” tách rời. Đây là một ví dụ cho thấy lựa chọn pattern đúng (observer ở đây) làm thuật toán phức tạp (topological build orchestration) tan thành mã ngắn và rõ.

Cập nhật: 2026-05-29