Skip to content

Commit e6d9cad

Browse files
committed
Split worker threads into Blueprints v1- and v2- specific
1 parent 59ca056 commit e6d9cad

File tree

7 files changed

+398
-328
lines changed

7 files changed

+398
-328
lines changed

packages/playground/client/src/blueprints-v1-handler.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,33 @@ import {
77
} from '.';
88
import { collectPhpLogs, logger } from '@php-wasm/logger';
99

10-
interface BootPlaygroundOptions extends StartPlaygroundOptions {
11-
playground: PlaygroundClient;
12-
progressTracker: ProgressTracker;
13-
}
1410
export class BlueprintsV1Handler {
15-
async bootPlayground({
16-
playground,
17-
blueprint,
18-
progressTracker,
19-
onBlueprintStepCompleted,
20-
corsProxy,
21-
mounts,
22-
sapiName,
23-
scope,
24-
shouldInstallWordPress,
25-
sqliteDriverVersion,
26-
onClientConnected,
27-
}: BootPlaygroundOptions) {
11+
constructor(private readonly options: StartPlaygroundOptions) {}
12+
13+
async bootPlayground(
14+
playground: PlaygroundClient,
15+
progressTracker: ProgressTracker
16+
) {
17+
const {
18+
blueprint,
19+
onBlueprintStepCompleted,
20+
corsProxy,
21+
mounts,
22+
sapiName,
23+
scope,
24+
shouldInstallWordPress,
25+
sqliteDriverVersion,
26+
onClientConnected,
27+
} = this.options;
2828
const executionProgress = progressTracker!.stage(0.5);
2929
const downloadProgress = progressTracker!.stage();
3030

3131
// Set a default blueprint if none is provided.
32-
if (!blueprint) {
33-
blueprint = {};
32+
let bp = blueprint as any;
33+
if (!bp) {
34+
bp = {} as any;
3435
}
35-
const compiled = await compileBlueprint(blueprint!, {
36+
const compiled = await compileBlueprint(bp, {
3637
progress: executionProgress,
3738
onStepCompleted: onBlueprintStepCompleted,
3839
corsProxy,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { ProgressTracker } from '@php-wasm/progress';
2+
import type { PlaygroundClient, StartPlaygroundOptions } from '.';
3+
import { collectPhpLogs, logger } from '@php-wasm/logger';
4+
5+
export class BlueprintsV2Handler {
6+
constructor(private readonly options: StartPlaygroundOptions) {}
7+
8+
async bootPlayground(
9+
playground: PlaygroundClient,
10+
progressTracker: ProgressTracker
11+
) {
12+
const {
13+
blueprint,
14+
onClientConnected,
15+
corsProxy,
16+
mounts,
17+
sapiName,
18+
scope,
19+
} = this.options;
20+
const downloadProgress = progressTracker!.stage();
21+
22+
// Connect the Comlink API client to the remote worker download monitor
23+
await playground.onDownloadProgress(downloadProgress.loadingListener);
24+
25+
await playground.boot({
26+
mounts,
27+
sapiName,
28+
scope: scope ?? Math.random().toFixed(16),
29+
corsProxyUrl: corsProxy,
30+
experimentalBlueprintsV2Runner: true,
31+
// Pass the declaration directly – the worker runs the V2 runner.
32+
blueprint: blueprint as any,
33+
} as any);
34+
35+
await playground.isReady();
36+
downloadProgress.finish();
37+
38+
collectPhpLogs(logger, playground);
39+
onClientConnected?.(playground);
40+
}
41+
}

packages/playground/client/src/index.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ export {
2525
export { phpVar, phpVars } from '@php-wasm/util';
2626
export type { PlaygroundClient, MountDescriptor };
2727

28-
import type { Blueprint, OnStepCompleted } from '@wp-playground/blueprints';
28+
import type {
29+
Blueprint,
30+
BlueprintV2Declaration,
31+
OnStepCompleted,
32+
} from '@wp-playground/blueprints';
2933
import { compileBlueprint, runBlueprintSteps } from '@wp-playground/blueprints';
3034
import { consumeAPI } from '@php-wasm/web';
3135
import { ProgressTracker } from '@php-wasm/progress';
@@ -35,13 +39,14 @@ import { additionalRemoteOrigins } from './additional-remote-origins';
3539
// eslint-disable-next-line @nx/enforce-module-boundaries
3640
import { remoteDevServerHost, remoteDevServerPort } from '../../build-config';
3741
import { BlueprintsV1Handler } from './blueprints-v1-handler';
42+
import { BlueprintsV2Handler } from './blueprints-v2-handler';
3843

3944
export interface StartPlaygroundOptions {
4045
iframe: HTMLIFrameElement;
4146
remoteUrl: string;
4247
progressTracker?: ProgressTracker;
4348
disableProgressBar?: boolean;
44-
blueprint?: Blueprint;
49+
blueprint?: Blueprint | BlueprintV2Declaration;
4550
onBlueprintStepCompleted?: OnStepCompleted;
4651
/**
4752
* Called when the playground client is connected, but before the blueprint
@@ -83,6 +88,8 @@ export interface StartPlaygroundOptions {
8388
* Defaults to the latest development version.
8489
*/
8590
sqliteDriverVersion?: string;
91+
/** When true, use Blueprints v2 flow and dedicated worker */
92+
experimentalBlueprintsV2Runner?: boolean;
8693
}
8794

8895
/**
@@ -106,6 +113,7 @@ export async function startPlaygroundWeb(
106113

107114
remoteUrl = setQueryParams(remoteUrl, {
108115
progressbar: !disableProgressBar,
116+
runner: options.experimentalBlueprintsV2Runner ? 'v2' : 'v1',
109117
});
110118
progressTracker.setCaption('Preparing WordPress');
111119

@@ -123,12 +131,10 @@ export async function startPlaygroundWeb(
123131
await playground.isConnected();
124132
progressTracker.pipe(playground);
125133

126-
const handler = new BlueprintsV1Handler();
127-
await handler.bootPlayground({
128-
...options,
129-
progressTracker,
130-
playground,
131-
});
134+
const handler = options.experimentalBlueprintsV2Runner
135+
? new BlueprintsV2Handler(options)
136+
: new BlueprintsV1Handler(options);
137+
await handler.bootPlayground(playground, progressTracker);
132138

133139
progressTracker.finish();
134140

packages/playground/remote/src/lib/boot-playground-remote.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@ import ProgressBar from './progress-bar';
2222
// resolved by the browser at runtime to reflect the current origin.
2323
const origin = new URL('/', (import.meta || {}).url).origin;
2424

25+
// Select worker runtime (v1 or v2) based on query parameter
2526
// @ts-ignore
26-
import moduleWorkerUrl from './worker-thread?worker&url';
27+
import workerV1Url from './worker-thread-v1?worker&url';
28+
// @ts-ignore
29+
import workerV2Url from './worker-thread-v2?worker&url';
2730

28-
export const workerUrl: string = new URL(moduleWorkerUrl, origin) + '';
31+
function getWorkerUrl(): string {
32+
const runner = new URL(document.location.href).searchParams.get('runner');
33+
const isV2 = runner === 'v2';
34+
const selected = isV2 ? workerV2Url : workerV1Url;
35+
return new URL(selected, origin) + '';
36+
}
2937

3038
// @ts-ignore
3139
import serviceWorkerPath from '../../service-worker.ts?worker&url';
@@ -93,7 +101,7 @@ export async function bootPlaygroundRemote() {
93101
}
94102

95103
const phpWorkerApi = consumeAPI<PlaygroundWorkerEndpoint>(
96-
await spawnPHPWorkerThread(workerUrl)
104+
await spawnPHPWorkerThread(getWorkerUrl())
97105
);
98106

99107
const wpFrame = document.querySelector('#wp') as HTMLIFrameElement;
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { EmscriptenDownloadMonitor } from '@php-wasm/progress';
2+
import { exposeAPI } from '@php-wasm/web';
3+
import { PlaygroundWorkerEndpoint } from './worker-thread';
4+
import { randomString } from '@php-wasm/util';
5+
import {
6+
getSqliteDriverModuleDetails,
7+
getWordPressModuleDetails,
8+
LatestMinifiedWordPressVersion,
9+
LatestSqliteDriverVersion,
10+
MinifiedWordPressVersionsList,
11+
} from '@wp-playground/wordpress-builds';
12+
import { directoryHandleFromMountDevice } from '@wp-playground/storage';
13+
import { bootJustWordPress } from '@wp-playground/wordpress';
14+
import { createDirectoryHandleMountHandler } from '@php-wasm/web';
15+
import type { PHP } from '@php-wasm/universal';
16+
/* @ts-ignore */
17+
import { corsProxyUrl as defaultCorsProxyUrl } from 'virtual:cors-proxy-url';
18+
import type { WorkerBootOptions } from './worker-thread';
19+
20+
// post message to parent
21+
self.postMessage('worker-script-started');
22+
23+
const downloadMonitor = new EmscriptenDownloadMonitor();
24+
25+
class ArtifactExpiredError extends Error {
26+
constructor(message = 'GitHub artifact expired') {
27+
super(message);
28+
this.name = 'ArtifactExpiredError';
29+
}
30+
}
31+
32+
class PlaygroundWorkerEndpointV1 extends PlaygroundWorkerEndpoint {
33+
override async boot({
34+
scope,
35+
mounts = [],
36+
wpVersion = LatestMinifiedWordPressVersion,
37+
sqliteDriverVersion = LatestSqliteDriverVersion,
38+
phpVersion,
39+
sapiName = 'cli',
40+
withICU = false,
41+
withNetworking = true,
42+
shouldInstallWordPress = true,
43+
corsProxyUrl,
44+
}: WorkerBootOptions) {
45+
if (this.booted) {
46+
throw new Error('Playground already booted');
47+
}
48+
if (corsProxyUrl === undefined) {
49+
corsProxyUrl = defaultCorsProxyUrl as any;
50+
}
51+
this.booted = true;
52+
this.scope = scope;
53+
54+
try {
55+
const endpoint = this;
56+
const knownRemoteAssetPaths = new Set<string>();
57+
const siteUrl = this.computeSiteUrl(scope);
58+
const requestHandler = await this.createRequestHandler({
59+
siteUrl,
60+
sapiName,
61+
withICU,
62+
corsProxyUrl,
63+
knownRemoteAssetPaths,
64+
withNetworking,
65+
phpVersion: phpVersion!,
66+
});
67+
68+
this.requestedWordPressVersion = wpVersion;
69+
wpVersion = MinifiedWordPressVersionsList.includes(wpVersion)
70+
? wpVersion
71+
: LatestMinifiedWordPressVersion;
72+
73+
let wordPressRequest: Promise<Response> | null = null;
74+
if (shouldInstallWordPress) {
75+
if (this.requestedWordPressVersion!.startsWith('http')) {
76+
wordPressRequest = this.downloadMonitor
77+
.monitorFetch(
78+
fetch(this.requestedWordPressVersion as string)
79+
)
80+
.then((response) => {
81+
if (response.ok) {
82+
return response;
83+
}
84+
let json: any = null;
85+
return response.json().then(
86+
(parsedJson) => {
87+
json = parsedJson;
88+
if (
89+
json &&
90+
json.error === 'artifact_expired'
91+
) {
92+
throw new ArtifactExpiredError();
93+
}
94+
throw new Error(
95+
`Failed to download WordPress ZIP (HTTP ${response.status})`
96+
);
97+
},
98+
() => {
99+
throw new Error(
100+
`Failed to download WordPress ZIP (HTTP ${response.status})`
101+
);
102+
}
103+
);
104+
});
105+
} else {
106+
const wpDetails = getWordPressModuleDetails(wpVersion);
107+
this.downloadMonitor.expectAssets({
108+
[wpDetails.url]: wpDetails.size,
109+
});
110+
wordPressRequest = this.downloadMonitor.monitorFetch(
111+
fetch(wpDetails.url)
112+
);
113+
}
114+
}
115+
116+
let sqliteIntegrationRequest: Promise<Response> | null = null;
117+
const sqliteDriverModuleDetails = getSqliteDriverModuleDetails(
118+
sqliteDriverVersion!
119+
);
120+
this.downloadMonitor.expectAssets({
121+
[sqliteDriverModuleDetails.url]: sqliteDriverModuleDetails.size,
122+
});
123+
sqliteIntegrationRequest = this.downloadMonitor.monitorFetch(
124+
fetch(sqliteDriverModuleDetails.url)
125+
);
126+
127+
await bootJustWordPress(requestHandler, {
128+
siteUrl,
129+
constants: shouldInstallWordPress
130+
? {
131+
WP_DEBUG: true,
132+
WP_DEBUG_LOG: true,
133+
WP_DEBUG_DISPLAY: false,
134+
AUTH_KEY: randomString(40),
135+
SECURE_AUTH_KEY: randomString(40),
136+
LOGGED_IN_KEY: randomString(40),
137+
NONCE_KEY: randomString(40),
138+
AUTH_SALT: randomString(40),
139+
SECURE_AUTH_SALT: randomString(40),
140+
LOGGED_IN_SALT: randomString(40),
141+
NONCE_SALT: randomString(40),
142+
}
143+
: {},
144+
wordPressZip: shouldInstallWordPress
145+
? wordPressRequest!
146+
.then((r) => r.blob())
147+
.then((b) => new File([b], 'wp.zip'))
148+
: undefined,
149+
sqliteIntegrationPluginZip: sqliteIntegrationRequest
150+
? sqliteIntegrationRequest
151+
.then((r) => r.blob())
152+
.then((b) => new File([b], 'sqlite.zip'))
153+
: undefined,
154+
hooks: {
155+
async beforeWordPressFiles(php: PHP) {
156+
for (const mount of mounts) {
157+
const handle = await directoryHandleFromMountDevice(
158+
mount.device
159+
);
160+
const unmount = await php.mount(
161+
mount.mountpoint,
162+
createDirectoryHandleMountHandler(handle, {
163+
initialSync: {
164+
direction: mount.initialSyncDirection,
165+
},
166+
})
167+
);
168+
endpoint.unmounts[mount.mountpoint] = unmount;
169+
}
170+
},
171+
},
172+
});
173+
174+
await this.finalizeAfterBoot(
175+
requestHandler,
176+
withNetworking,
177+
knownRemoteAssetPaths
178+
);
179+
setApiReady();
180+
} catch (e) {
181+
setAPIError(e as Error);
182+
throw e as Error;
183+
}
184+
}
185+
}
186+
187+
const [setApiReady, setAPIError] = exposeAPI(
188+
new PlaygroundWorkerEndpointV1(downloadMonitor)
189+
);

0 commit comments

Comments
 (0)