Skip to content

Commit 83a581e

Browse files
owengregsonclaude
andcommitted
fix: playground worker readiness handshake, resolve CI/deploy race
Worker now sends a `{ ready: true }` signal after module initialization, and the UI waits for it before enabling the obfuscate button. This prevents posting messages to a not-yet-loaded ESM worker. Added `build:browser` step to ci.yml so the deployed site includes the worker bundle. Restored `paths-ignore` on deploy.yml so CI and Deploy workflows don't both trigger on packages/ruam/** changes and race for the deploy-pages concurrency group. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 3184f5f commit 83a581e

4 files changed

Lines changed: 54 additions & 32 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ jobs:
5555
if: github.ref == 'refs/heads/main'
5656
run: cp packages/ruam/stats.json apps/web/public/stats.json
5757

58+
# Build browser worker bundle for the playground
59+
- name: Build browser worker bundle
60+
if: github.ref == 'refs/heads/main'
61+
run: npx turbo run build:browser --filter=ruam
62+
5863
- name: Build site with stats
5964
if: github.ref == 'refs/heads/main'
6065
run: npx turbo run build --filter=web

.github/workflows/deploy.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ on:
55
branches: [main]
66
paths:
77
- 'apps/web/**'
8-
- 'packages/ruam/src/**'
98
- '.github/workflows/deploy.yml'
9+
paths-ignore:
10+
- 'packages/ruam/**'
1011

1112
concurrency:
1213
group: deploy-pages

apps/web/components/Playground.tsx

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -321,14 +321,31 @@ function useWorker() {
321321
const workerRef = useRef<Worker | null>(null);
322322
const idRef = useRef(0);
323323
const [ready, setReady] = useState(false);
324+
const [initError, setInitError] = useState<string | null>(null);
324325

325326
useEffect(() => {
326327
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
327328
const w = new Worker(`${basePath}/ruam-worker.mjs`, {
328329
type: "module",
329330
});
330331
workerRef.current = w;
331-
setReady(true);
332+
333+
// Wait for the worker module to signal readiness
334+
const onReady = (e: MessageEvent) => {
335+
if (e.data.ready) {
336+
w.removeEventListener("message", onReady);
337+
w.removeEventListener("error", onInitErr);
338+
setReady(true);
339+
}
340+
};
341+
const onInitErr = (e: ErrorEvent) => {
342+
w.removeEventListener("message", onReady);
343+
w.removeEventListener("error", onInitErr);
344+
setInitError(e.message || "Worker failed to load");
345+
};
346+
w.addEventListener("message", onReady);
347+
w.addEventListener("error", onInitErr);
348+
332349
return () => {
333350
w.terminate();
334351
workerRef.current = null;
@@ -382,7 +399,7 @@ function useWorker() {
382399
[]
383400
);
384401

385-
return { ready, obfuscate };
402+
return { ready, obfuscate, initError };
386403
}
387404

388405
// --- Playground component ---
@@ -423,7 +440,7 @@ export default function Playground() {
423440
{ current: null }
424441
);
425442

426-
const { ready: workerReady, obfuscate } = useWorker();
443+
const { ready: workerReady, obfuscate, initError: workerError } = useWorker();
427444

428445
const allLoaded = inputLoaded && outputLoaded && workerReady;
429446

@@ -814,13 +831,26 @@ export default function Playground() {
814831
{!allLoaded && (
815832
<div className="fixed inset-0 z-50 flex items-center justify-center bg-void/80 backdrop-blur-sm">
816833
<div className="flex flex-col items-center gap-4">
817-
<FontAwesomeIcon
818-
icon={faSpinner}
819-
className="h-8 w-8 animate-spin text-accent"
820-
/>
821-
<p className="font-mono text-sm text-smoke">
822-
Loading Ruam engine...
823-
</p>
834+
{workerError ? (
835+
<>
836+
<p className="font-mono text-sm text-alert">
837+
Failed to load Ruam engine
838+
</p>
839+
<p className="max-w-md text-center font-mono text-xs text-ash">
840+
{workerError}
841+
</p>
842+
</>
843+
) : (
844+
<>
845+
<FontAwesomeIcon
846+
icon={faSpinner}
847+
className="h-8 w-8 animate-spin text-accent"
848+
/>
849+
<p className="font-mono text-sm text-smoke">
850+
Loading Ruam engine...
851+
</p>
852+
</>
853+
)}
824854
</div>
825855
</div>
826856
)}

packages/ruam/src/browser-worker.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
* Web Worker entry point for the Ruam playground.
33
*
44
* Receives `{ code, options }` messages, runs obfuscation, and posts
5-
* back `{ result }` or `{ error }` responses.
5+
* back `{ result }` or `{ error }` responses. Posts a `{ ready: true }`
6+
* message on load so the main thread knows the module is initialized.
67
*
78
* @module browser-worker
89
*/
@@ -16,34 +17,19 @@ interface WorkerRequest {
1617
options?: VmObfuscationOptions;
1718
}
1819

19-
interface WorkerResponseOk {
20-
id: number;
21-
result: string;
22-
elapsed: number;
23-
}
24-
25-
interface WorkerResponseErr {
26-
id: number;
27-
error: string;
28-
}
29-
3020
self.onmessage = (e: MessageEvent<WorkerRequest>) => {
3121
const { id, code, options } = e.data;
3222
const start = performance.now();
3323
try {
3424
const result = obfuscateCode(code, options);
3525
const elapsed = Math.round(performance.now() - start);
36-
(self as unknown as Worker).postMessage({
37-
id,
38-
result,
39-
elapsed,
40-
} satisfies WorkerResponseOk);
26+
(self as unknown as Worker).postMessage({ id, result, elapsed });
4127
} catch (err: unknown) {
4228
const message =
4329
err instanceof Error ? err.message : String(err);
44-
(self as unknown as Worker).postMessage({
45-
id,
46-
error: message,
47-
} satisfies WorkerResponseErr);
30+
(self as unknown as Worker).postMessage({ id, error: message });
4831
}
4932
};
33+
34+
// Signal that the module has loaded and onmessage is set
35+
(self as unknown as Worker).postMessage({ ready: true });

0 commit comments

Comments
 (0)