@cognipilot/rumoca-core 0.9.4

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.
@@ -0,0 +1,394 @@
1
+ // Web Worker for Rumoca WASM with rayon threading support
2
+ // This worker runs WASM functions that use Atomics.wait (not allowed on main thread)
3
+
4
+ // Cache-busting query propagated from worker URL (e.g. rumoca_worker.js?v=...).
5
+ const workerUrl = new URL(self.location.href);
6
+ const cacheBust = workerUrl.searchParams.get('v') || '';
7
+ const withCacheBust = (path) =>
8
+ cacheBust ? `${path}?v=${encodeURIComponent(cacheBust)}` : path;
9
+
10
+ let init;
11
+ let wasm_init;
12
+ let get_version;
13
+ let get_builtin_targets;
14
+ let compile_to_json;
15
+ let compile_with_project_sources;
16
+ let sync_project_sources;
17
+ let get_source_root_statuses;
18
+ let get_simulation_models;
19
+ let compile_with_source_roots;
20
+ let load_source_roots;
21
+ let clear_source_root_cache;
22
+ let get_source_root_document_count;
23
+ let export_parsed_source_roots_binary;
24
+ let merge_parsed_source_roots_binary;
25
+ let lsp_diagnostics;
26
+ let lsp_hover;
27
+ let lsp_completion;
28
+ let lsp_completion_with_timing;
29
+ let lsp_definition;
30
+ let lsp_document_symbols;
31
+ let lsp_code_actions;
32
+ let lsp_semantic_tokens;
33
+ let lsp_semantic_token_legend;
34
+ let list_classes;
35
+ let get_class_info;
36
+ let render_target;
37
+ let simulate_model = null;
38
+ let simulate_model_with_project_sources = null;
39
+ let lower_model_to_solve_json = null;
40
+ let wasmModuleLoaded = false;
41
+ let activeRequestId = null;
42
+
43
+ // Lazy diffsol (stiff/implicit) addon — a separate relaxed-SIMD module, loaded
44
+ // only when a BDF solve is requested, so this worker (and the package) stay
45
+ // universal. Returns the addon's exports, or rejects on browsers without
46
+ // relaxed-SIMD.
47
+ let diffsolAddonPromise = null;
48
+ function loadDiffsolAddon() {
49
+ if (!diffsolAddonPromise) {
50
+ diffsolAddonPromise = (async () => {
51
+ const mod = await import(withCacheBust('./rumoca_bind_wasm_diffsol.js'));
52
+ await mod.default();
53
+ return mod;
54
+ })();
55
+ diffsolAddonPromise.catch(() => {
56
+ diffsolAddonPromise = null;
57
+ });
58
+ }
59
+ return diffsolAddonPromise;
60
+ }
61
+
62
+ // Run a stiff (BDF/diffsol) simulation: the main module lowers the model to a
63
+ // `{ solve_model, t_end, dt }` payload, the addon simulates it. Returns the
64
+ // same JSON string shape as `simulate_model`.
65
+ async function simulateViaDiffsol(source, model, tEnd, dt) {
66
+ if (typeof lower_model_to_solve_json !== 'function') {
67
+ throw new Error('this build cannot lower models for the diffsol addon');
68
+ }
69
+ let addon;
70
+ try {
71
+ addon = await loadDiffsolAddon();
72
+ } catch (err) {
73
+ throw new Error(
74
+ 'the stiff (BDF/diffsol) solver needs a browser with WebAssembly '
75
+ + 'relaxed-SIMD (Chrome 114+, Firefox 120+, Safari 16.4+). '
76
+ + `Use RK45 instead. (${err && err.message ? err.message : err})`,
77
+ );
78
+ }
79
+ const prep = lower_model_to_solve_json(source, model, tEnd, dt);
80
+ return addon.simulate_solve_model_diffsol(prep);
81
+ }
82
+
83
+ function canUseSharedWasmThreads() {
84
+ return typeof self.crossOriginIsolated === 'boolean'
85
+ && self.crossOriginIsolated
86
+ && typeof SharedArrayBuffer !== 'undefined';
87
+ }
88
+
89
+ async function loadWasmModule() {
90
+ if (wasmModuleLoaded) return;
91
+ const mod = await import(withCacheBust('./rumoca_bind_wasm.js'));
92
+ init = mod.default;
93
+ wasm_init = mod.wasm_init;
94
+ get_version = mod.get_version;
95
+ get_builtin_targets = mod.get_builtin_targets;
96
+ compile_to_json = mod.compile_to_json;
97
+ compile_with_project_sources = mod.compile_with_project_sources;
98
+ sync_project_sources = mod.sync_project_sources;
99
+ get_source_root_statuses = mod.get_source_root_statuses;
100
+ get_simulation_models = mod.get_simulation_models;
101
+ compile_with_source_roots = mod.compile_with_source_roots;
102
+ load_source_roots = mod.load_source_roots;
103
+ clear_source_root_cache = mod.clear_source_root_cache;
104
+ get_source_root_document_count = mod.get_source_root_document_count;
105
+ export_parsed_source_roots_binary = mod.export_parsed_source_roots_binary;
106
+ merge_parsed_source_roots_binary = mod.merge_parsed_source_roots_binary;
107
+ lsp_diagnostics = mod.lsp_diagnostics;
108
+ lsp_hover = mod.lsp_hover;
109
+ lsp_completion = mod.lsp_completion;
110
+ lsp_completion_with_timing = mod.lsp_completion_with_timing;
111
+ lsp_definition = mod.lsp_definition;
112
+ lsp_document_symbols = mod.lsp_document_symbols;
113
+ lsp_code_actions = mod.lsp_code_actions;
114
+ lsp_semantic_tokens = mod.lsp_semantic_tokens;
115
+ lsp_semantic_token_legend = mod.lsp_semantic_token_legend;
116
+ list_classes = mod.list_classes;
117
+ get_class_info = mod.get_class_info;
118
+ render_target = mod.render_target;
119
+ if (typeof mod.simulate_model === 'function') {
120
+ simulate_model = mod.simulate_model;
121
+ }
122
+ if (typeof mod.simulate_model_with_project_sources === 'function') {
123
+ simulate_model_with_project_sources = mod.simulate_model_with_project_sources;
124
+ }
125
+ if (typeof mod.lower_model_to_solve_json === 'function') {
126
+ lower_model_to_solve_json = mod.lower_model_to_solve_json;
127
+ }
128
+ wasmModuleLoaded = true;
129
+ }
130
+
131
+ let initialized = false;
132
+
133
+ // Intercept console.log to forward progress messages to main thread
134
+ const originalLog = console.log;
135
+ console.log = function(...args) {
136
+ originalLog.apply(console, args);
137
+ // Forward WASM progress messages to main thread
138
+ const message = args.join(' ');
139
+ if (message.includes('[WASM]') && message.includes('parsing')) {
140
+ // Extract progress info: "[WASM] wasm::project: parsing 50/500 (10%)"
141
+ const scopeMatch = message.match(/\[WASM\]\s+([^:]+(?:::[^:]+)*):\s+/);
142
+ const match = message.match(/parsing (\d+)\/(\d+) \((\d+)%\)/);
143
+ if (match) {
144
+ self.postMessage({
145
+ id: activeRequestId,
146
+ progress: true,
147
+ kind: 'parse',
148
+ scope: scopeMatch ? scopeMatch[1] : '',
149
+ message,
150
+ current: parseInt(match[1]),
151
+ total: parseInt(match[2]),
152
+ percent: parseInt(match[3])
153
+ });
154
+ }
155
+ }
156
+ };
157
+
158
+ async function initialize() {
159
+ if (initialized) return true;
160
+
161
+ try {
162
+ console.log('[Worker] Loading WASM module...');
163
+ await loadWasmModule();
164
+ await init({ module_or_path: withCacheBust('./rumoca_bind_wasm_bg.wasm') });
165
+
166
+ const requestedThreads = navigator.hardwareConcurrency || 4;
167
+ const numThreads = canUseSharedWasmThreads() ? requestedThreads : 0;
168
+ if (numThreads > 0) {
169
+ console.log('[Worker] Initializing thread pool...');
170
+ } else {
171
+ console.warn('[Worker] Shared WASM threads unavailable; using single-thread mode.');
172
+ }
173
+ await wasm_init(numThreads);
174
+ if (numThreads > 0) {
175
+ console.log(`[Worker] Thread pool initialized with ${numThreads} threads`);
176
+ }
177
+ initialized = true;
178
+ return true;
179
+ } catch (e) {
180
+ console.error('[Worker] Initialization failed:', e);
181
+ return false;
182
+ }
183
+ }
184
+
185
+ // Initialize and report status
186
+ initialize().then(success => {
187
+ self.postMessage({ ready: true, success });
188
+ });
189
+
190
+ // Handle messages from main thread
191
+ self.onmessage = async (e) => {
192
+ const { id, action, source, modelName, line, character, daeJson, tEnd, dt } = e.data;
193
+
194
+ if (!initialized) {
195
+ self.postMessage({ id, error: 'Worker not initialized' });
196
+ return;
197
+ }
198
+
199
+ try {
200
+ let result;
201
+ activeRequestId = id;
202
+ const command = e.data.command || '';
203
+ self.postMessage({
204
+ id,
205
+ progress: true,
206
+ kind: 'request',
207
+ phase: 'start',
208
+ action,
209
+ command,
210
+ });
211
+ switch (action) {
212
+ case 'languageCommand': {
213
+ const payload = e.data.payload || {};
214
+ if (typeof sync_project_sources === 'function' && typeof payload.projectSources === 'string') {
215
+ sync_project_sources(payload.projectSources);
216
+ }
217
+ switch (command) {
218
+ case 'rumoca.language.getSourceRootDocumentCount':
219
+ result = get_source_root_document_count();
220
+ break;
221
+ case 'rumoca.language.diagnostics':
222
+ result = lsp_diagnostics(payload.source || '');
223
+ break;
224
+ case 'rumoca.language.hover':
225
+ result = lsp_hover(payload.source || '', payload.line, payload.character);
226
+ break;
227
+ case 'rumoca.language.completion':
228
+ result = lsp_completion(payload.source || '', payload.line, payload.character);
229
+ break;
230
+ case 'rumoca.language.completionWithTiming':
231
+ result = lsp_completion_with_timing(payload.source || '', payload.line, payload.character);
232
+ break;
233
+ case 'rumoca.language.definition':
234
+ result = lsp_definition(payload.source || '', payload.line, payload.character);
235
+ break;
236
+ case 'rumoca.language.documentSymbols':
237
+ result = lsp_document_symbols(payload.source || '');
238
+ break;
239
+ case 'rumoca.language.codeActions':
240
+ result = lsp_code_actions(
241
+ payload.source || '',
242
+ payload.rangeStartLine,
243
+ payload.rangeStartCharacter,
244
+ payload.rangeEndLine,
245
+ payload.rangeEndCharacter,
246
+ payload.diagnosticsJson || '[]',
247
+ );
248
+ break;
249
+ case 'rumoca.language.semanticTokens':
250
+ result = lsp_semantic_tokens(payload.source || '');
251
+ break;
252
+ case 'rumoca.language.semanticTokenLegend':
253
+ result = lsp_semantic_token_legend();
254
+ break;
255
+ case 'rumoca.language.listClasses':
256
+ result = list_classes();
257
+ break;
258
+ case 'rumoca.language.getClassInfo':
259
+ result = get_class_info(payload.qualifiedName);
260
+ break;
261
+ default:
262
+ throw new Error(`Unknown language command: ${command}`);
263
+ }
264
+ break;
265
+ }
266
+ case 'projectCommand': {
267
+ const payload = e.data.payload || {};
268
+ switch (command) {
269
+ case 'rumoca.project.getSimulationModels':
270
+ result = get_simulation_models(payload.source || '', payload.defaultModel || '');
271
+ break;
272
+ case 'rumoca.project.startSimulation':
273
+ if (!simulate_model) {
274
+ throw new Error('Simulation not available in this WASM build. Rebuild with rumoca-sim (diffsol feature enabled).');
275
+ }
276
+ if ((payload.solver || '') === 'bdf') {
277
+ // Stiff path: route to the lazy diffsol addon (the
278
+ // main module can't simulate bdf — it has no diffsol
279
+ // runtime). Project-local sources aren't supported on
280
+ // this path yet.
281
+ result = await simulateViaDiffsol(
282
+ payload.source || '',
283
+ payload.modelName || 'Model',
284
+ payload.tEnd || 1.0,
285
+ payload.dt || 0,
286
+ );
287
+ } else if (
288
+ simulate_model_with_project_sources
289
+ && typeof payload.projectSources === 'string'
290
+ && payload.projectSources.trim()
291
+ && payload.projectSources.trim() !== '{}'
292
+ ) {
293
+ result = simulate_model_with_project_sources(
294
+ payload.source || '',
295
+ payload.modelName || 'Model',
296
+ payload.projectSources,
297
+ payload.tEnd || 1.0,
298
+ payload.dt || 0,
299
+ payload.solver || 'auto',
300
+ );
301
+ } else {
302
+ result = simulate_model(
303
+ payload.source || '',
304
+ payload.modelName || 'Model',
305
+ payload.tEnd || 1.0,
306
+ payload.dt || 0,
307
+ payload.solver || 'auto',
308
+ );
309
+ }
310
+ break;
311
+ default:
312
+ throw new Error(`Unknown project command: ${command}`);
313
+ }
314
+ break;
315
+ }
316
+ case 'workspaceCommand': {
317
+ const payload = e.data.payload || {};
318
+ switch (command) {
319
+ case 'rumoca.workspace.getVersion':
320
+ result = get_version();
321
+ break;
322
+ case 'rumoca.workspace.getBuiltinTargets':
323
+ result = get_builtin_targets();
324
+ break;
325
+ case 'rumoca.workspace.compile':
326
+ result = compile_to_json(payload.source || '', payload.modelName || 'Model');
327
+ break;
328
+ case 'rumoca.workspace.compileWithProjectSources':
329
+ result = compile_with_project_sources(
330
+ payload.source || '',
331
+ payload.modelName || 'Model',
332
+ payload.projectSources || '{}',
333
+ );
334
+ break;
335
+ case 'rumoca.workspace.compileWithSourceRoots':
336
+ result = compile_with_source_roots(
337
+ payload.source || '',
338
+ payload.modelName || 'Model',
339
+ payload.sourceRoots || '{}',
340
+ );
341
+ break;
342
+ case 'rumoca.workspace.loadSourceRoots':
343
+ result = load_source_roots(payload.sourceRoots || '{}');
344
+ break;
345
+ case 'rumoca.workspace.getSourceRootStatuses':
346
+ result = get_source_root_statuses();
347
+ break;
348
+ case 'rumoca.workspace.exportParsedSourceRootsBinary':
349
+ result = export_parsed_source_roots_binary(payload.urisJson || '[]');
350
+ break;
351
+ case 'rumoca.workspace.mergeParsedSourceRootsBinary':
352
+ result = merge_parsed_source_roots_binary(payload.bytes || new Uint8Array());
353
+ break;
354
+ case 'rumoca.workspace.clearSourceRootCache':
355
+ clear_source_root_cache();
356
+ result = 'OK';
357
+ break;
358
+ case 'rumoca.workspace.renderTarget':
359
+ result = render_target(
360
+ payload.daeJson,
361
+ payload.modelName || 'Model',
362
+ payload.target || '',
363
+ payload.manifest || '',
364
+ payload.templates || '{}',
365
+ );
366
+ break;
367
+ default:
368
+ throw new Error(`Unknown workspace command: ${command}`);
369
+ }
370
+ break;
371
+ }
372
+ default:
373
+ throw new Error(`Unknown action: ${action}`);
374
+ }
375
+ self.postMessage({
376
+ id,
377
+ progress: true,
378
+ kind: 'request',
379
+ phase: 'finish',
380
+ action,
381
+ command,
382
+ });
383
+ activeRequestId = null;
384
+ if (result instanceof Uint8Array) {
385
+ self.postMessage({ id, success: true, result }, [result.buffer]);
386
+ return;
387
+ }
388
+ self.postMessage({ id, success: true, result });
389
+ } catch (e) {
390
+ activeRequestId = null;
391
+ console.error('[Worker] Error:', e);
392
+ self.postMessage({ id, error: e.message || String(e) });
393
+ }
394
+ };