@cfasim-ui/docs 0.3.18 → 0.4.1

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.
@@ -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 WorkerMessage {
9
+ interface RunMessage {
23
10
  id: number;
24
- type?: "run" | "loadModule";
25
- python?: string;
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: Omit<WorkerMessage, "id">,
35
+ msg: OutgoingMessage,
33
36
  ): Promise<{ result?: unknown; error?: string }> {
34
- const { promise, resolve } = getPromiseAndResolve<{
35
- result?: unknown;
36
- error?: string;
37
- }>();
38
- const idWorker = getId();
39
-
40
- function listener(event: MessageEvent<TransferableResponse>) {
41
- if (event.data?.id !== idWorker) {
42
- return;
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.removeEventListener("message", listener);
45
- if (event.data.error) {
46
- resolve({ error: event.data.error });
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
- export type WorkerName = "baseline" | "intervention";
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
- function createWorker(): Worker {
60
- return new Worker(new URL("./pyodide.worker.ts", import.meta.url), {
61
- type: "module",
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: Record<WorkerName, Worker> = {
66
- baseline: createWorker(),
67
- intervention: createWorker(),
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
- export async function loadModuleOnWorker(
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
- return requestResponse(workers[worker], {
75
- type: "loadModule",
76
- module: moduleName,
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
- // Load module on all workers so any worker can run code that depends on it
84
- const results = await Promise.all(
85
- Object.values(workers).map((w) =>
86
- requestResponse(w, { type: "loadModule", module: moduleName }),
87
- ),
88
- );
89
- return results[0];
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?: WorkerName,
144
+ worker: WorkerName = DEFAULT_WORKER,
96
145
  ): Promise<{ result?: unknown; error?: string }> {
97
- const target = worker ? workers[worker] : workers.baseline;
98
- return requestResponse(target, {
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
+ }
@@ -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 { asyncRunPython, loadModule } from "./pyodideWorkerApi.js";
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 argNames = context ? Object.keys(context) : [];
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 plain = Object.fromEntries(
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 {