Skip to content

Commit cbbd3fb

Browse files
kubeclaude
andcommitted
Use inline workers (?worker&inline) for simulation and language server
Vite library mode emits worker assets as separate files that consumers can't resolve. Using ?worker&inline inlines the compiled worker code as a string and creates a Blob URL at runtime, making workers self-contained in the library bundle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2d1d42a commit cbbd3fb

2 files changed

Lines changed: 117 additions & 104 deletions

File tree

libs/@hashintel/petrinaut/src/lsp/worker/use-language-client.ts

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ import type {
1212
SignatureHelp,
1313
} from "./protocol";
1414

15+
/** Dynamically import and instantiate the language server worker (inlined as blob URL). */
16+
async function createLanguageServerWorker() {
17+
const LanguageServerWorker = await import(
18+
"./language-server.worker.ts?worker&inline"
19+
);
20+
// eslint-disable-next-line new-cap
21+
return new LanguageServerWorker.default();
22+
}
23+
1524
type Pending = {
1625
resolve: (result: never) => void;
1726
reject: (error: Error) => void;
@@ -56,40 +65,38 @@ export function useLanguageClient(): LanguageClientApi {
5665
>(null);
5766

5867
useEffect(() => {
59-
const worker = new Worker(
60-
new URL("./language-server.worker.ts", import.meta.url),
61-
{
62-
type: "module",
63-
},
64-
);
65-
66-
worker.onmessage = (event: MessageEvent<ServerMessage>) => {
67-
const msg = event.data;
68-
69-
if ("id" in msg) {
70-
// Response to a request
71-
const pending = pendingRef.current.get(msg.id);
72-
if (!pending) {
73-
return;
68+
void (async () => {
69+
const worker = await createLanguageServerWorker();
70+
71+
worker.onmessage = (event: MessageEvent<ServerMessage>) => {
72+
const msg = event.data;
73+
74+
if ("id" in msg) {
75+
// Response to a request
76+
const pending = pendingRef.current.get(msg.id);
77+
if (!pending) {
78+
return;
79+
}
80+
pendingRef.current.delete(msg.id);
81+
82+
if ("error" in msg) {
83+
pending.reject(new Error(msg.error.message));
84+
} else {
85+
pending.resolve(msg.result as never);
86+
}
87+
} else if ("method" in msg) {
88+
// Server-pushed notification
89+
diagnosticsCallbackRef.current?.(msg.params);
7490
}
75-
pendingRef.current.delete(msg.id);
91+
};
7692

77-
if ("error" in msg) {
78-
pending.reject(new Error(msg.error.message));
79-
} else {
80-
pending.resolve(msg.result as never);
81-
}
82-
} else if ("method" in msg) {
83-
// Server-pushed notification
84-
diagnosticsCallbackRef.current?.(msg.params);
85-
}
86-
};
93+
workerRef.current = worker;
94+
})();
8795

88-
workerRef.current = worker;
8996
const pending = pendingRef.current;
9097

9198
return () => {
92-
worker.terminate();
99+
workerRef.current?.terminate();
93100
workerRef.current = null;
94101
for (const entry of pending.values()) {
95102
entry.reject(new Error("Worker terminated"));

libs/@hashintel/petrinaut/src/simulation/worker/use-simulation-worker.ts

Lines changed: 82 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ import type { SDCPN } from "../../core/types/sdcpn";
1616
import type { InitialMarking, SimulationFrame } from "../context";
1717
import type { ToMainMessage, ToWorkerMessage } from "./messages";
1818

19+
/** Dynamically import and instantiate the simulation worker (inlined as blob URL). */
20+
async function createSimulationWorker(): Promise<Worker> {
21+
const SimulationWorker = await import("./simulation.worker.ts?worker&inline");
22+
// eslint-disable-next-line new-cap
23+
return new SimulationWorker.default();
24+
}
25+
1926
/**
2027
* Status of the simulation worker.
2128
*/
@@ -127,84 +134,83 @@ export function useSimulationWorker(): {
127134

128135
// Initialize worker on mount
129136
useEffect(() => {
130-
const worker = new Worker(
131-
new URL("./simulation.worker.ts", import.meta.url),
132-
{ type: "module" },
133-
);
134-
135-
worker.onmessage = (event: MessageEvent<ToMainMessage>) => {
136-
const message = event.data;
137-
138-
switch (message.type) {
139-
case "ready":
140-
setState((prev) => ({
141-
...prev,
142-
status: prev.status === "initializing" ? "ready" : prev.status,
143-
}));
144-
// Resolve pending initialization promise
145-
if (pendingInitRef.current) {
146-
pendingInitRef.current.resolve();
147-
pendingInitRef.current = null;
148-
}
149-
break;
150-
151-
case "frame":
152-
setState((prev) => ({
153-
...prev,
154-
frames: [...prev.frames, message.frame],
155-
}));
156-
break;
157-
158-
case "frames":
159-
setState((prev) => ({
160-
...prev,
161-
frames: [...prev.frames, ...message.frames],
162-
}));
163-
break;
164-
165-
case "complete":
166-
setState((prev) => ({
167-
...prev,
168-
status: "complete",
169-
}));
170-
break;
171-
172-
case "paused":
173-
setState((prev) => ({
174-
...prev,
175-
status: "paused",
176-
}));
177-
break;
178-
179-
case "error":
180-
setState((prev) => ({
181-
...prev,
182-
status: "error",
183-
error: message.message,
184-
errorItemId: message.itemId,
185-
}));
186-
// Reject pending initialization promise if this error occurred during init
187-
if (pendingInitRef.current) {
188-
pendingInitRef.current.reject(new Error(message.message));
189-
pendingInitRef.current = null;
190-
}
191-
break;
192-
}
193-
};
194-
195-
worker.onerror = (error) => {
196-
setState((prev) => ({
197-
...prev,
198-
status: "error",
199-
error: error.message || "Worker error",
200-
errorItemId: null,
201-
}));
202-
};
203-
204-
workerRef.current = worker;
137+
void (async () => {
138+
const worker = await createSimulationWorker();
139+
140+
worker.onmessage = (event: MessageEvent<ToMainMessage>) => {
141+
const message = event.data;
142+
143+
switch (message.type) {
144+
case "ready":
145+
setState((prev) => ({
146+
...prev,
147+
status: prev.status === "initializing" ? "ready" : prev.status,
148+
}));
149+
// Resolve pending initialization promise
150+
if (pendingInitRef.current) {
151+
pendingInitRef.current.resolve();
152+
pendingInitRef.current = null;
153+
}
154+
break;
155+
156+
case "frame":
157+
setState((prev) => ({
158+
...prev,
159+
frames: [...prev.frames, message.frame],
160+
}));
161+
break;
162+
163+
case "frames":
164+
setState((prev) => ({
165+
...prev,
166+
frames: [...prev.frames, ...message.frames],
167+
}));
168+
break;
169+
170+
case "complete":
171+
setState((prev) => ({
172+
...prev,
173+
status: "complete",
174+
}));
175+
break;
176+
177+
case "paused":
178+
setState((prev) => ({
179+
...prev,
180+
status: "paused",
181+
}));
182+
break;
183+
184+
case "error":
185+
setState((prev) => ({
186+
...prev,
187+
status: "error",
188+
error: message.message,
189+
errorItemId: message.itemId,
190+
}));
191+
// Reject pending initialization promise if this error occurred during init
192+
if (pendingInitRef.current) {
193+
pendingInitRef.current.reject(new Error(message.message));
194+
pendingInitRef.current = null;
195+
}
196+
break;
197+
}
198+
};
199+
200+
worker.onerror = (error) => {
201+
setState((prev) => ({
202+
...prev,
203+
status: "error",
204+
error: error.message || "Worker error",
205+
errorItemId: null,
206+
}));
207+
};
208+
209+
workerRef.current = worker;
210+
})();
205211

206212
return () => {
207-
worker.terminate();
213+
workerRef.current?.terminate();
208214
};
209215
}, []);
210216

0 commit comments

Comments
 (0)