@cfasim-ui/docs 0.3.17 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/charts/BarChart/BarChart.md +189 -0
- package/charts/BarChart/BarChart.vue +829 -0
- package/charts/LineChart/LineChart.vue +68 -288
- package/charts/_shared/axes.ts +69 -0
- package/charts/_shared/computeTicks.ts +42 -0
- package/charts/_shared/index.ts +20 -0
- package/charts/_shared/seriesCsv.ts +68 -0
- package/charts/_shared/useChartMenu.ts +72 -0
- package/charts/_shared/useChartPadding.ts +37 -0
- package/charts/_shared/useChartSize.ts +49 -0
- package/charts/_shared/useChartTooltip.ts +152 -0
- package/charts/index.ts +5 -0
- package/components/NumberInput/NumberInput.md +52 -0
- package/components/NumberInput/NumberInput.vue +5 -0
- package/index.json +18 -1
- package/package.json +1 -1
- package/pyodide/index.ts +2 -0
- package/pyodide/pyodide.worker.ts +109 -72
- package/pyodide/pyodideWorkerApi.ts +157 -63
- package/pyodide/useModel.ts +10 -21
|
@@ -5,13 +5,25 @@ import {
|
|
|
5
5
|
} from "@cfasim-ui/shared";
|
|
6
6
|
import type { ColumnDescriptor, ModelOutputsWire } from "@cfasim-ui/shared";
|
|
7
7
|
|
|
8
|
-
interface
|
|
8
|
+
interface RunMessage {
|
|
9
9
|
id: number;
|
|
10
|
-
type?: "run"
|
|
11
|
-
python
|
|
12
|
-
module?: string;
|
|
10
|
+
type?: "run";
|
|
11
|
+
python: string;
|
|
13
12
|
context?: Record<string, unknown>;
|
|
14
13
|
}
|
|
14
|
+
interface CallMessage {
|
|
15
|
+
id: number;
|
|
16
|
+
type: "call";
|
|
17
|
+
module: string;
|
|
18
|
+
fn: string;
|
|
19
|
+
kwargs?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
interface LoadModuleMessage {
|
|
22
|
+
id: number;
|
|
23
|
+
type: "loadModule";
|
|
24
|
+
module: string;
|
|
25
|
+
}
|
|
26
|
+
type WorkerMessage = RunMessage | CallMessage | LoadModuleMessage;
|
|
15
27
|
|
|
16
28
|
let wheelMap: Record<string, string> = {};
|
|
17
29
|
|
|
@@ -92,24 +104,29 @@ function installAllWheels(): Promise<void> {
|
|
|
92
104
|
return installPromise;
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
const modulePromises = new Map<string, Promise<any>>();
|
|
96
109
|
|
|
97
110
|
function ensureModule(
|
|
98
111
|
pyodide: Awaited<typeof pyodideReadyPromise>,
|
|
99
112
|
moduleName: string,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
): Promise<any> {
|
|
115
|
+
let p = modulePromises.get(moduleName);
|
|
116
|
+
if (!p) {
|
|
117
|
+
if (!wheelMap[moduleName]) {
|
|
118
|
+
return Promise.reject(new Error(`Unknown module: ${moduleName}`));
|
|
119
|
+
}
|
|
120
|
+
p = (async () => {
|
|
104
121
|
await installAllWheels();
|
|
105
|
-
pyodide.pyimport(moduleName);
|
|
122
|
+
return pyodide.pyimport(moduleName);
|
|
106
123
|
})();
|
|
107
|
-
|
|
124
|
+
p.catch(() => {
|
|
108
125
|
modulePromises.delete(moduleName);
|
|
109
126
|
});
|
|
110
|
-
modulePromises.set(moduleName,
|
|
127
|
+
modulePromises.set(moduleName, p);
|
|
111
128
|
}
|
|
112
|
-
return
|
|
129
|
+
return p;
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
// Map Python struct format characters to TypedArray constructors
|
|
@@ -127,18 +144,20 @@ const FORMAT_TO_TYPED_ARRAY: Record<
|
|
|
127
144
|
d: Float64Array,
|
|
128
145
|
};
|
|
129
146
|
|
|
147
|
+
// Copy a Pyodide getBuffer() result into a JS-owned typed array. Releases the
|
|
148
|
+
// underlying Python buffer; the returned view's ArrayBuffer is safe to transfer.
|
|
130
149
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
|
-
function
|
|
150
|
+
function copyPyBuffer(proxy: any): ArrayBufferView {
|
|
132
151
|
const pyBuffer = proxy.getBuffer();
|
|
133
152
|
const Ctor = FORMAT_TO_TYPED_ARRAY[pyBuffer.format] ?? Float64Array;
|
|
134
|
-
const
|
|
153
|
+
const view = new Ctor(
|
|
135
154
|
pyBuffer.data.buffer.slice(
|
|
136
155
|
pyBuffer.data.byteOffset,
|
|
137
156
|
pyBuffer.data.byteOffset + pyBuffer.data.byteLength,
|
|
138
157
|
),
|
|
139
158
|
);
|
|
140
159
|
pyBuffer.release();
|
|
141
|
-
return
|
|
160
|
+
return view;
|
|
142
161
|
}
|
|
143
162
|
|
|
144
163
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -161,7 +180,7 @@ function convertModelOutputs(jsResult: any): ModelOutputsWire | null {
|
|
|
161
180
|
for (const buf of wire.buffers) {
|
|
162
181
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
182
|
if (buf && typeof buf === "object" && (buf as any).getBuffer) {
|
|
164
|
-
buffers.push(
|
|
183
|
+
buffers.push(copyPyBuffer(buf).buffer as ArrayBuffer);
|
|
165
184
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
185
|
if ((buf as any).destroy) (buf as any).destroy();
|
|
167
186
|
} else if (buf instanceof ArrayBuffer) {
|
|
@@ -187,76 +206,94 @@ function convertModelOutputs(jsResult: any): ModelOutputsWire | null {
|
|
|
187
206
|
return { __modelOutputs: true, outputs };
|
|
188
207
|
}
|
|
189
208
|
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
|
+
function convertResult(rawResult: any): unknown {
|
|
211
|
+
if (!rawResult || typeof rawResult.destroy !== "function") return rawResult;
|
|
212
|
+
try {
|
|
213
|
+
if (typeof rawResult.toJs === "function") {
|
|
214
|
+
return rawResult.toJs({ dict_converter: Object.fromEntries });
|
|
215
|
+
}
|
|
216
|
+
if (typeof rawResult.getBuffer === "function") {
|
|
217
|
+
return copyPyBuffer(rawResult);
|
|
218
|
+
}
|
|
219
|
+
return rawResult.toString();
|
|
220
|
+
} finally {
|
|
221
|
+
rawResult.destroy();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function send(id: number, result: unknown) {
|
|
226
|
+
const modelOutputs = convertModelOutputs(result);
|
|
227
|
+
if (modelOutputs) {
|
|
228
|
+
postModelOutputsWithTransfer(self, id, modelOutputs);
|
|
229
|
+
} else {
|
|
230
|
+
postWithTransfer(self, id, result);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
190
234
|
self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
|
|
191
235
|
const pyodide = await pyodideReadyPromise;
|
|
192
|
-
const
|
|
236
|
+
const msg = event.data;
|
|
237
|
+
const { id } = msg;
|
|
193
238
|
|
|
194
239
|
try {
|
|
195
|
-
if (type === "loadModule"
|
|
196
|
-
await ensureModule(pyodide,
|
|
240
|
+
if (msg.type === "loadModule") {
|
|
241
|
+
await ensureModule(pyodide, msg.module);
|
|
197
242
|
postWithTransfer(self, id, true);
|
|
198
243
|
return;
|
|
199
244
|
}
|
|
200
245
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
246
|
+
if (msg.type === "call") {
|
|
247
|
+
const mod = await ensureModule(pyodide, msg.module);
|
|
248
|
+
const t0 = performance.now();
|
|
249
|
+
// mod[fn] creates a fresh PyProxy on every access — release in finally.
|
|
250
|
+
const pyFn = mod[msg.fn];
|
|
251
|
+
let result: unknown;
|
|
252
|
+
try {
|
|
253
|
+
if (typeof pyFn !== "function") {
|
|
254
|
+
throw new Error(`Module ${msg.module} has no function ${msg.fn}`);
|
|
255
|
+
}
|
|
256
|
+
const rawResult = msg.kwargs ? pyFn.callKwargs(msg.kwargs) : pyFn();
|
|
257
|
+
result = convertResult(rawResult);
|
|
258
|
+
} finally {
|
|
259
|
+
if (pyFn && typeof pyFn.destroy === "function") pyFn.destroy();
|
|
260
|
+
}
|
|
261
|
+
const tEnd = performance.now();
|
|
262
|
+
console.log(
|
|
263
|
+
`[pyodide-worker] ${msg.module}.${msg.fn} ${
|
|
264
|
+
Math.round((tEnd - t0) * 10) / 10
|
|
265
|
+
}ms`,
|
|
266
|
+
);
|
|
267
|
+
send(id, result);
|
|
268
|
+
return;
|
|
205
269
|
}
|
|
206
270
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const tPython = performance.now();
|
|
216
|
-
|
|
217
|
-
// Destroy PyProxy if returned to prevent memory leaks
|
|
218
|
-
let result = rawResult;
|
|
219
|
-
if (rawResult && typeof rawResult === "object" && rawResult.destroy) {
|
|
220
|
-
if (rawResult.toJs) {
|
|
221
|
-
result = rawResult.toJs({ dict_converter: Object.fromEntries });
|
|
222
|
-
} else if (rawResult.getBuffer) {
|
|
223
|
-
// Single numpy array: use getBuffer() for direct typed array access
|
|
224
|
-
const pyBuffer = rawResult.getBuffer();
|
|
225
|
-
const Ctor = FORMAT_TO_TYPED_ARRAY[pyBuffer.format] ?? Float64Array;
|
|
226
|
-
result = new Ctor(
|
|
227
|
-
pyBuffer.data.buffer.slice(
|
|
228
|
-
pyBuffer.data.byteOffset,
|
|
229
|
-
pyBuffer.data.byteOffset + pyBuffer.data.byteLength,
|
|
230
|
-
),
|
|
231
|
-
);
|
|
232
|
-
pyBuffer.release();
|
|
233
|
-
} else {
|
|
234
|
-
result = rawResult.toString();
|
|
271
|
+
// type === "run" or omitted
|
|
272
|
+
let globals;
|
|
273
|
+
if (msg.context) {
|
|
274
|
+
const dict = pyodide.globals.get("dict");
|
|
275
|
+
try {
|
|
276
|
+
globals = dict(Object.entries(msg.context));
|
|
277
|
+
} finally {
|
|
278
|
+
dict.destroy();
|
|
235
279
|
}
|
|
236
|
-
rawResult.destroy();
|
|
237
280
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
281
|
+
const t0 = performance.now();
|
|
282
|
+
let result: unknown;
|
|
283
|
+
try {
|
|
284
|
+
const rawResult = pyodide.runPython(
|
|
285
|
+
msg.python,
|
|
286
|
+
globals ? { globals } : undefined,
|
|
287
|
+
);
|
|
288
|
+
result = convertResult(rawResult);
|
|
289
|
+
} finally {
|
|
290
|
+
if (globals && globals.destroy) globals.destroy();
|
|
242
291
|
}
|
|
243
|
-
|
|
244
|
-
const tConvert = performance.now();
|
|
245
|
-
const bench = {
|
|
246
|
-
python_ms: Math.round((tPython - t0) * 10) / 10,
|
|
247
|
-
convert_ms: Math.round((tConvert - tPython) * 10) / 10,
|
|
248
|
-
};
|
|
292
|
+
const tEnd = performance.now();
|
|
249
293
|
console.log(
|
|
250
|
-
`[pyodide-worker]
|
|
294
|
+
`[pyodide-worker] runPython ${Math.round((tEnd - t0) * 10) / 10}ms`,
|
|
251
295
|
);
|
|
252
|
-
|
|
253
|
-
// Check for ModelOutputs wire format
|
|
254
|
-
const modelOutputs = convertModelOutputs(result);
|
|
255
|
-
if (modelOutputs) {
|
|
256
|
-
postModelOutputsWithTransfer(self, id, modelOutputs);
|
|
257
|
-
} else {
|
|
258
|
-
postWithTransfer(self, id, result);
|
|
259
|
-
}
|
|
296
|
+
send(id, result);
|
|
260
297
|
} catch (error) {
|
|
261
298
|
postErrorWithTransfer(self, id, error);
|
|
262
299
|
}
|
|
@@ -1,102 +1,196 @@
|
|
|
1
1
|
import type { TransferableResponse } from "@cfasim-ui/shared";
|
|
2
2
|
import { unwrapResponse } from "@cfasim-ui/shared";
|
|
3
3
|
|
|
4
|
-
interface PromiseResolver<T> {
|
|
5
|
-
promise: Promise<T>;
|
|
6
|
-
resolve: (value: T) => void;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function getPromiseAndResolve<T>(): PromiseResolver<T> {
|
|
10
|
-
let resolve: (value: T) => void;
|
|
11
|
-
const promise = new Promise<T>((res) => {
|
|
12
|
-
resolve = res;
|
|
13
|
-
});
|
|
14
|
-
return { promise, resolve: resolve! };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
4
|
let lastId = 0;
|
|
18
5
|
function getId(): number {
|
|
19
6
|
return ++lastId;
|
|
20
7
|
}
|
|
21
8
|
|
|
22
|
-
interface
|
|
9
|
+
interface RunMessage {
|
|
23
10
|
id: number;
|
|
24
|
-
type?: "run"
|
|
25
|
-
python
|
|
26
|
-
module?: string;
|
|
11
|
+
type?: "run";
|
|
12
|
+
python: string;
|
|
27
13
|
context?: Record<string, unknown>;
|
|
28
14
|
}
|
|
15
|
+
interface CallMessage {
|
|
16
|
+
id: number;
|
|
17
|
+
type: "call";
|
|
18
|
+
module: string;
|
|
19
|
+
fn: string;
|
|
20
|
+
kwargs?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
interface LoadModuleMessage {
|
|
23
|
+
id: number;
|
|
24
|
+
type: "loadModule";
|
|
25
|
+
module: string;
|
|
26
|
+
}
|
|
27
|
+
type WorkerMessage = RunMessage | CallMessage | LoadModuleMessage;
|
|
28
|
+
type OutgoingMessage =
|
|
29
|
+
| Omit<RunMessage, "id">
|
|
30
|
+
| Omit<CallMessage, "id">
|
|
31
|
+
| Omit<LoadModuleMessage, "id">;
|
|
29
32
|
|
|
30
33
|
function requestResponse(
|
|
31
34
|
worker: Worker,
|
|
32
|
-
msg:
|
|
35
|
+
msg: OutgoingMessage,
|
|
33
36
|
): Promise<{ result?: unknown; error?: string }> {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const id = getId();
|
|
39
|
+
function listener(event: MessageEvent<TransferableResponse>) {
|
|
40
|
+
if (event.data?.id !== id) return;
|
|
41
|
+
worker.removeEventListener("message", listener);
|
|
42
|
+
if (event.data.error) {
|
|
43
|
+
resolve({ error: event.data.error });
|
|
44
|
+
} else {
|
|
45
|
+
resolve({ result: unwrapResponse(event.data) });
|
|
46
|
+
}
|
|
43
47
|
}
|
|
44
|
-
worker.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} else {
|
|
48
|
-
resolve({ result: unwrapResponse(event.data) });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
worker.addEventListener("message", listener);
|
|
53
|
-
worker.postMessage({ id: idWorker, ...msg } as WorkerMessage);
|
|
54
|
-
return promise;
|
|
48
|
+
worker.addEventListener("message", listener);
|
|
49
|
+
worker.postMessage({ id, ...msg } as WorkerMessage);
|
|
50
|
+
});
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Worker names are arbitrary string keys. Workers are spawned lazily on first
|
|
55
|
+
* use; each one is an independent Pyodide interpreter. Use distinct names when
|
|
56
|
+
* you want runs to execute in parallel without contention.
|
|
57
|
+
*/
|
|
58
|
+
export type WorkerName = string;
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
const DEFAULT_WORKER = "default";
|
|
61
|
+
|
|
62
|
+
interface WorkerEntry {
|
|
63
|
+
worker: Worker;
|
|
64
|
+
modules: Map<string, Promise<{ result?: unknown; error?: string }>>;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
const workers
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
67
|
+
const workers = new Map<string, WorkerEntry>();
|
|
68
|
+
// Modules registered via loadModule() — auto-installed on any newly spawned worker.
|
|
69
|
+
const sharedModules = new Set<string>();
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
function getWorker(name: string): WorkerEntry {
|
|
72
|
+
let entry = workers.get(name);
|
|
73
|
+
if (!entry) {
|
|
74
|
+
const worker = new Worker(new URL("./pyodide.worker.ts", import.meta.url), {
|
|
75
|
+
type: "module",
|
|
76
|
+
});
|
|
77
|
+
entry = { worker, modules: new Map() };
|
|
78
|
+
workers.set(name, entry);
|
|
79
|
+
for (const mod of sharedModules) {
|
|
80
|
+
ensureModuleOn(entry, mod);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return entry;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ensureModuleOn(
|
|
87
|
+
entry: WorkerEntry,
|
|
71
88
|
moduleName: string,
|
|
72
|
-
worker: WorkerName,
|
|
73
89
|
): Promise<{ result?: unknown; error?: string }> {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
let p = entry.modules.get(moduleName);
|
|
91
|
+
if (!p) {
|
|
92
|
+
p = requestResponse(entry.worker, {
|
|
93
|
+
type: "loadModule",
|
|
94
|
+
module: moduleName,
|
|
95
|
+
});
|
|
96
|
+
entry.modules.set(moduleName, p);
|
|
97
|
+
p.then((r) => {
|
|
98
|
+
if (r.error) entry.modules.delete(moduleName);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return p;
|
|
78
102
|
}
|
|
79
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Mark `moduleName` as a shared module: install it on every currently-spawned
|
|
106
|
+
* worker, and on any future worker the moment it spawns. Returns once the
|
|
107
|
+
* default worker has finished installing.
|
|
108
|
+
*/
|
|
80
109
|
export async function loadModule(
|
|
81
110
|
moduleName: string,
|
|
82
111
|
): Promise<{ result?: unknown; error?: string }> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
112
|
+
sharedModules.add(moduleName);
|
|
113
|
+
// Always install on the default worker — that's what callers expect when
|
|
114
|
+
// the function name has no "OnWorker" suffix.
|
|
115
|
+
const defaultInstall = ensureModuleOn(getWorker(DEFAULT_WORKER), moduleName);
|
|
116
|
+
const others: Array<Promise<unknown>> = [];
|
|
117
|
+
for (const [name, entry] of workers) {
|
|
118
|
+
if (name !== DEFAULT_WORKER) others.push(ensureModuleOn(entry, moduleName));
|
|
119
|
+
}
|
|
120
|
+
await Promise.all(others);
|
|
121
|
+
return defaultInstall;
|
|
90
122
|
}
|
|
91
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Install `moduleName` on a single worker. Spawns the worker if needed. Does
|
|
126
|
+
* not mark the module as shared — other workers won't auto-load it. Use this
|
|
127
|
+
* when one worker should diverge from others.
|
|
128
|
+
*/
|
|
129
|
+
export async function loadModuleOnWorker(
|
|
130
|
+
moduleName: string,
|
|
131
|
+
worker: WorkerName,
|
|
132
|
+
): Promise<{ result?: unknown; error?: string }> {
|
|
133
|
+
return ensureModuleOn(getWorker(worker), moduleName);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Run an arbitrary Python script. `context` keys are exposed as Python globals.
|
|
138
|
+
* Prefer {@link callPython} when calling a function on a loaded module — it
|
|
139
|
+
* skips Python source parsing on every invocation.
|
|
140
|
+
*/
|
|
92
141
|
export async function asyncRunPython(
|
|
93
142
|
script: string,
|
|
94
143
|
context?: Record<string, unknown>,
|
|
95
|
-
worker
|
|
144
|
+
worker: WorkerName = DEFAULT_WORKER,
|
|
96
145
|
): Promise<{ result?: unknown; error?: string }> {
|
|
97
|
-
|
|
98
|
-
|
|
146
|
+
return requestResponse(getWorker(worker).worker, {
|
|
147
|
+
type: "run",
|
|
99
148
|
python: script,
|
|
100
149
|
context,
|
|
101
150
|
});
|
|
102
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Call `module.fn(**kwargs)` on the named worker. Faster than asyncRunPython
|
|
155
|
+
* for repeated invocations: the worker caches the imported module and dispatches
|
|
156
|
+
* directly to the cached function rather than re-parsing source each call.
|
|
157
|
+
*/
|
|
158
|
+
export async function callPython(
|
|
159
|
+
module: string,
|
|
160
|
+
fn: string,
|
|
161
|
+
kwargs?: Record<string, unknown>,
|
|
162
|
+
worker: WorkerName = DEFAULT_WORKER,
|
|
163
|
+
): Promise<{ result?: unknown; error?: string }> {
|
|
164
|
+
const entry = getWorker(worker);
|
|
165
|
+
const installResult = await ensureModuleOn(entry, module);
|
|
166
|
+
if (installResult.error) return installResult;
|
|
167
|
+
return requestResponse(entry.worker, {
|
|
168
|
+
type: "call",
|
|
169
|
+
module,
|
|
170
|
+
fn,
|
|
171
|
+
kwargs,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Pre-spawn the named workers and (optionally) pre-install modules on each.
|
|
177
|
+
* Pyodide takes a few seconds to boot, so warming workers up front lets the
|
|
178
|
+
* first call return immediately. Call this once near app startup when you
|
|
179
|
+
* know you'll need parallel interpreters (e.g. for side-by-side comparisons).
|
|
180
|
+
*
|
|
181
|
+
* Modules listed in `modules` are also registered as shared, so any worker
|
|
182
|
+
* spawned later will auto-install them.
|
|
183
|
+
*/
|
|
184
|
+
export async function warmWorkers(options: {
|
|
185
|
+
workers: WorkerName[];
|
|
186
|
+
modules?: string[];
|
|
187
|
+
}): Promise<void> {
|
|
188
|
+
const modules = options.modules ?? [];
|
|
189
|
+
for (const mod of modules) sharedModules.add(mod);
|
|
190
|
+
const installs: Array<Promise<unknown>> = [];
|
|
191
|
+
for (const name of options.workers) {
|
|
192
|
+
const entry = getWorker(name);
|
|
193
|
+
for (const mod of modules) installs.push(ensureModuleOn(entry, mod));
|
|
194
|
+
}
|
|
195
|
+
await Promise.all(installs);
|
|
196
|
+
}
|
package/pyodide/useModel.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { ref, toRaw, toValue, watch } from "vue";
|
|
2
2
|
import type { MaybeRef } from "vue";
|
|
3
3
|
import type { ModelOutput } from "@cfasim-ui/shared";
|
|
4
|
-
import {
|
|
4
|
+
import { callPython, loadModule } from "./pyodideWorkerApi.js";
|
|
5
|
+
|
|
6
|
+
function plainKwargs(
|
|
7
|
+
ctx: Record<string, unknown> | undefined,
|
|
8
|
+
): Record<string, unknown> | undefined {
|
|
9
|
+
if (!ctx) return undefined;
|
|
10
|
+
return Object.fromEntries(Object.entries(ctx).map(([k, v]) => [k, toRaw(v)]));
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
export function useModel<T = unknown>(moduleName: string) {
|
|
7
14
|
const result = ref<T>();
|
|
@@ -18,17 +25,7 @@ export function useModel<T = unknown>(moduleName: string) {
|
|
|
18
25
|
error.value = undefined;
|
|
19
26
|
try {
|
|
20
27
|
await loaded;
|
|
21
|
-
const
|
|
22
|
-
const callArgs = argNames.join(", ");
|
|
23
|
-
const plainContext = context
|
|
24
|
-
? Object.fromEntries(
|
|
25
|
-
Object.entries(context).map(([k, v]) => [k, toRaw(v)]),
|
|
26
|
-
)
|
|
27
|
-
: undefined;
|
|
28
|
-
const response = await asyncRunPython(
|
|
29
|
-
`import ${moduleName}\n${moduleName}.${fn}(${callArgs})`,
|
|
30
|
-
plainContext,
|
|
31
|
-
);
|
|
28
|
+
const response = await callPython(moduleName, fn, plainKwargs(context));
|
|
32
29
|
if (response.error) {
|
|
33
30
|
error.value = response.error;
|
|
34
31
|
} else {
|
|
@@ -56,15 +53,7 @@ export function useModel<T = unknown>(moduleName: string) {
|
|
|
56
53
|
outputsError.value = undefined;
|
|
57
54
|
try {
|
|
58
55
|
await loaded;
|
|
59
|
-
const
|
|
60
|
-
Object.entries(p).map(([k, v]) => [k, toRaw(v)]),
|
|
61
|
-
);
|
|
62
|
-
const argNames = Object.keys(plain);
|
|
63
|
-
const callArgs = argNames.join(", ");
|
|
64
|
-
const response = await asyncRunPython(
|
|
65
|
-
`import ${moduleName}\n${moduleName}.${fn}(${callArgs})`,
|
|
66
|
-
plain,
|
|
67
|
-
);
|
|
56
|
+
const response = await callPython(moduleName, fn, plainKwargs(p));
|
|
68
57
|
if (response.error) {
|
|
69
58
|
outputsError.value = response.error;
|
|
70
59
|
} else {
|