File size: 4,569 Bytes
73f2932
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17411d4
 
 
 
 
73f2932
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
02baa9b
 
 
 
73f2932
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17411d4
 
 
 
 
73f2932
 
 
17411d4
 
 
 
 
73f2932
 
 
 
 
 
 
 
 
 
 
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
const PYODIDE_URL = "https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.mjs";

import managerCode from "../../../backend/src/manager.py?raw";
import logicCode from "../../../backend/src/logic.py?raw";
import type { CsvParseResult, CsvSettings, DataPoints, DecisionBoundaryResult, DecisionBoundarySettings, ModelSettings } from "./types";


export class PyodideBackend {
  private initialized: boolean = false;
  private initPromise: Promise<void> | null = null;

  private chain: Promise<void> = Promise.resolve();

  private pyodide: any = null;
  private manager: any = null;
  private errorHandler: ((message: string) => void) | null = null;

  setErrorHandler(handler: (message: string) => void): void {
    this.errorHandler = handler;
  }

  async init(): Promise<void> {
    if (this.initialized) {
      return;
    }
    if (this.initPromise) {
      return this.initPromise;
    }

    this.initPromise = (async () => {
      const { loadPyodide } = await import(/* @vite-ignore */ PYODIDE_URL);

      this.pyodide = await loadPyodide({
        indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.1/full/"
      });

      await this.pyodide.loadPackage(["numpy", "scikit-learn", "pandas"]);

      this.pyodide.FS.writeFile("logic.py", logicCode);
      this.pyodide.FS.writeFile("manager.py", managerCode);
      this.pyodide.runPython(`from manager import Manager; manager = Manager();`);
      this.manager = this.pyodide.globals.get("manager");

      if (!this.manager) {
        throw new Error("Failed to initialize pyodide manager");
      }

      console.log("Pyodide initialized");
      this.initialized = true;
    })();

    return this.initPromise;
  }

  async setDataset2d(dataset: DataPoints): Promise<void> {
    console.log("setting datset 2d");
    await this.init();

    const inputs = dataset.xPoints.map((x, i) => [x, dataset.yPoints[i]]);
    const outputs = dataset.labels;
    await this.handleCall<void>("handle_set_dataset", { inputs, outputs });
  }

  async setModelConfig(settings: ModelSettings): Promise<void> {
    await this.handleCall<void>(
      "handle_set_model_config", { type: settings.type, arguments: settings.arguments }
    );
  }

  async buildModel(): Promise<void> {
    await this.handleCall<void>("handle_build_model");
  }

  async getDecisionBoundary(settings: DecisionBoundarySettings): Promise<DecisionBoundaryResult> {
    return await this.handleCall<DecisionBoundaryResult>("handle_get_decision_boundary", settings);
  }

  async setDatasetCsv(buffer: ArrayBuffer, settings: CsvSettings): Promise<CsvParseResult> {
    return await this.handleCall<CsvParseResult>(
      "handle_set_dataset_csv", { buffer: buffer, settings }
    );
  }

  async setCsvSettings(settings: CsvSettings): Promise<CsvParseResult> {
    return await this.handleCall<CsvParseResult>("handle_set_csv_settings", settings);
  }

  async getDatasetCsv(): Promise<string> {
    return await this.handleCall<string>("handle_get_dataset_csv");
  }

  private enqueue<T>(fn: () => Promise<T>): Promise<T> {
    const next = this.chain.then(() => fn(), () => fn());
    this.chain = next.then(() => undefined, () => undefined);
    return next;
  }

  private async handleCall<T>(methodName: string, args?: unknown): Promise<T> {
    await this.init();

    return this.enqueue(async () => {
      let pyArgs: any = null;
      let pyResult: any = null;

      try {
        if (args !== undefined) {
          pyArgs = this.pyodide.toPy(args);
        }

        const fn = this.manager[methodName];
        if (!fn) {
          throw new Error(`Manager has no method named ${methodName}`);
        }

        pyResult = args === undefined ? fn.call(this.manager) : fn.call(this.manager, pyArgs);

        const result = pyResult && typeof pyResult.toJs === "function" 
          ? pyResult.toJs({ dict_converter: Object.fromEntries })
          : pyResult;

        if (result?.status === "USER_ERROR") {
          const message = result.message || "An error occurred in the backend";
          throw new Error(message);
        }
        
        return result as T;

      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        this.errorHandler?.(message);
        throw error instanceof Error ? error : new Error(message);

      } finally {
        if (pyArgs && typeof pyArgs.destroy === "function") {
          pyArgs.destroy();
        }
        if (pyResult && typeof pyResult.destroy === "function") {
          pyResult.destroy();
        }
      }
    });
  }
};