File size: 2,711 Bytes
9b9eafc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// Cầu nối main thread ↔ worker: gọi lệnh dạng Promise và phát sự kiện cho giao diện.

import type { WorkerAction, WorkerEvent } from '../shared/protocol';

export class WorkerError extends Error {
  code?: string;
  constructor(message: string, name?: string, code?: string) {
    super(message);
    this.name = name ?? 'Error';
    this.code = code;
  }
}

type Listener = (event: WorkerEvent) => void;

export class AgentClient {
  private worker: Worker;
  private nextId = 1;
  private pending = new Map<number, { resolve: (v: any) => void; reject: (e: Error) => void }>();
  private listeners = new Set<Listener>();
  /** Kết quả `initialized` đầu tiên — giao diện chờ nó trước khi cho phép thao tác. */
  readonly ready: Promise<Extract<WorkerEvent, { type: 'initialized' }>>;

  constructor() {
    this.worker = new Worker(new URL('../worker/agent.worker.ts', import.meta.url), { type: 'module' });
    let resolveReady!: (e: Extract<WorkerEvent, { type: 'initialized' }>) => void;
    this.ready = new Promise((resolve) => (resolveReady = resolve));

    this.worker.onmessage = ({ data }: MessageEvent<WorkerEvent>) => {
      if (data.type === 'reply') {
        const p = this.pending.get(data.id);
        if (!p) return;
        this.pending.delete(data.id);
        if (data.error !== undefined) p.reject(new WorkerError(data.error, data.errorName, data.errorCode));
        else p.resolve(data.value);
        return;
      }
      if (data.type === 'initialized') resolveReady(data);
      this.listeners.forEach((l) => l(data));
    };

    const fail = (message: string) => {
      const error = new WorkerError(message);
      this.pending.forEach((p) => p.reject(error));
      this.pending.clear();
      this.listeners.forEach((l) => l({ type: 'fatal', error: message }));
    };
    this.worker.onerror = (e) => fail(e.message || 'The background worker stopped unexpectedly. Reload the page.');
    this.worker.onmessageerror = () => fail('The background worker sent an unreadable message.');
  }

  call<T = unknown>(action: WorkerAction['action'], args?: Record<string, unknown>): Promise<T> {
    const id = this.nextId++;
    return new Promise<T>((resolve, reject) => {
      this.pending.set(id, { resolve, reject });
      this.worker.postMessage({ id, action, args });
    });
  }

  subscribe(listener: Listener): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
}

let singleton: AgentClient | undefined;

/** Một worker duy nhất cho cả ứng dụng (an toàn với React StrictMode). */
export function getClient(): AgentClient {
  return (singleton ??= new AgentClient());
}