From 6c12e788908449aa327f03736e7adf8b86bb8ed2 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Wed, 13 Sep 2023 22:01:54 -0400 Subject: [PATCH 01/56] Add pooling and request limiting to prevent memory leaks --- .../src/executors/built-script/executor.ts | 1 + .../src/executors/built-script/schema.d.ts | 1 + .../src/executors/built-script/schema.json | 5 ++ packages/wp-now/src/start-server.ts | 39 ++++++-- packages/wp-now/src/wp-now.ts | 90 ++++++++++++++++--- 5 files changed, 115 insertions(+), 21 deletions(-) diff --git a/packages/nx-extensions/src/executors/built-script/executor.ts b/packages/nx-extensions/src/executors/built-script/executor.ts index 4c93ad5e..11024ef6 100644 --- a/packages/nx-extensions/src/executors/built-script/executor.ts +++ b/packages/nx-extensions/src/executors/built-script/executor.ts @@ -7,6 +7,7 @@ const dirname = __dirname; export default async function runExecutor(options: BuiltScriptExecutorSchema) { const args = [ + ...(options.debug ? ['--inspect-brk'] : []), '--loader', join(dirname, 'loader.mjs'), options.scriptPath, diff --git a/packages/nx-extensions/src/executors/built-script/schema.d.ts b/packages/nx-extensions/src/executors/built-script/schema.d.ts index f03329c1..29aaa10c 100644 --- a/packages/nx-extensions/src/executors/built-script/schema.d.ts +++ b/packages/nx-extensions/src/executors/built-script/schema.d.ts @@ -1,4 +1,5 @@ export interface BuiltScriptExecutorSchema { scriptPath: string; + debug: boolean; __unparsed__: string; } // eslint-disable-line diff --git a/packages/nx-extensions/src/executors/built-script/schema.json b/packages/nx-extensions/src/executors/built-script/schema.json index 42ccfb44..35f44f4d 100644 --- a/packages/nx-extensions/src/executors/built-script/schema.json +++ b/packages/nx-extensions/src/executors/built-script/schema.json @@ -10,6 +10,11 @@ "description": "Path of the script to run.", "x-prompt": "What script would you like to run?" }, + "debug": { + "type": "boolean", + "description": "Use devtools as a debugger.", + "x-prompt": "Would you like to use devtools?" + }, "__unparsed__": { "hidden": true, "type": "array", diff --git a/packages/wp-now/src/start-server.ts b/packages/wp-now/src/start-server.ts index 13cb0a2c..509d9179 100644 --- a/packages/wp-now/src/start-server.ts +++ b/packages/wp-now/src/start-server.ts @@ -50,7 +50,7 @@ function shouldCompress(_, res) { } export async function startServer( - options: WPNowOptions = {} + options: WPNowOptions = {numberOfPhpInstances: 5} ): Promise { if (!fs.existsSync(options.projectPath)) { throw new Error( @@ -62,7 +62,7 @@ export async function startServer( app.use(compression({ filter: shouldCompress })); app.use(addTrailingSlash('/wp-admin')); const port = await portFinder.getOpenPort(); - const { php, options: wpNowOptions } = await startWPNow(options); + const { php, instanceMap, options: wpNowOptions, refresher } = await startWPNow(options); app.use('/', async (req, res) => { try { @@ -103,12 +103,35 @@ export async function startServer( ), body: body as string, }; - const resp = await php.request(data); - res.statusCode = resp.httpStatusCode; - Object.keys(resp.headers).forEach((key) => { - res.setHeader(key, resp.headers[key]); - }); - res.end(resp.bytes); + + await refresher(); + + const instanceMapSorted = [...instanceMap.entries()] + .sort((a, b) => a[1].requests - b[1].requests) + + for (const [php, info] of instanceMapSorted) { + if (!info.active || info.busy) { + continue; + } + + info.requests++; + info.busy = true; + + const resp = await php.request(data); + + info.busy = false; + + res.statusCode = resp.httpStatusCode; + + Object.keys(resp.headers).forEach((key) => { + res.setHeader(key, resp.headers[key]); + }); + + res.end(resp.bytes); + + break; + } + } catch (e) { output?.trace(e); } diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index d794cfd9..2900ba5a 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -44,8 +44,15 @@ async function applyToInstances(phpInstances: NodePHP[], callback: Function) { export default async function startWPNow( options: Partial = {} -): Promise<{ php: NodePHP; phpInstances: NodePHP[]; options: WPNowOptions }> { +): Promise<{ + php: NodePHP; + phpInstances: NodePHP[]; + instanceMap: Map; + options: WPNowOptions; + refresher: Function; +}> { const { documentRoot } = options; + const nodePHPOptions: PHPLoaderOptions = { requestHandler: { documentRoot, @@ -67,11 +74,32 @@ export default async function startWPNow( }; const phpInstances = []; - for (let i = 0; i < Math.max(options.numberOfPhpInstances, 1); i++) { - phpInstances.push( - await NodePHP.load(options.phpVersion, nodePHPOptions) - ); - } + const instanceMap = new Map; + + const fillInstances = async () : Promise> => { + const newPHPs : Set = new Set; + // for (let i = 0; i < Math.max(options.numberOfPhpInstances, 1); i++) + while (instanceMap.size < options.numberOfPhpInstances) { + console.error('Respawning...'); + const php = await NodePHP.load(options.phpVersion, nodePHPOptions); + phpInstances.push(php); + instanceMap.set(php, {requests: 0, started: Date.now(), initialized: false, active: true, busy: false}); + newPHPs.add(php); + } + return newPHPs; + }; + + const clearInstances = async () => { + for (const [php, info] of instanceMap) { + if (info.requests > 20) { + info.active = false; + instanceMap.delete(php); + } + } + }; + + await fillInstances(); + const php = phpInstances[0]; phpInstances.forEach((_php) => { @@ -86,13 +114,25 @@ export default async function startWPNow( output?.log(`directory: ${options.projectPath}`); output?.log(`mode: ${options.mode}`); output?.log(`php: ${options.phpVersion}`); + if (options.mode === WPNowMode.INDEX) { - await applyToInstances(phpInstances, async (_php) => { - runIndexMode(_php, options); - }); - return { php, phpInstances, options }; + + const refresher = async () => { + await clearInstances(); + const newPHPs = await fillInstances(); + await applyToInstances([...newPHPs.values()], async (_php) => { + instanceMap.get(php).initialized = true; + runIndexMode(_php, options); + }); + }; + + await refresher(); + + return { php, phpInstances, instanceMap, options, refresher }; } + output?.log(`wp: ${options.wordPressVersion}`); + await Promise.all([ downloadWordPress(options.wordPressVersion), downloadSqliteIntegrationPlugin(), @@ -107,7 +147,13 @@ export default async function startWPNow( } const isFirstTimeProject = !fs.existsSync(options.wpContentPath); - await applyToInstances(phpInstances, async (_php) => { + + const setUpWordPress = async (_php) => { + + const info = instanceMap.get(_php); + + info.initialized = true; + switch (options.mode) { case WPNowMode.WP_CONTENT: await runWpContentMode(_php, options); @@ -127,8 +173,24 @@ export default async function startWPNow( case WPNowMode.PLAYGROUND: await runWpPlaygroundMode(_php, options); break; - } - }); + } + }; + + const refresher = async () => { + await clearInstances(); + + const newPHPs = await fillInstances(); + + await applyToInstances([...newPHPs.values()], async _php => { + setUpWordPress(_php); + await login(_php, { + username: 'admin', + password: 'password', + }); + }); + }; + + await applyToInstances([...instanceMap.keys()], setUpWordPress); if (options.blueprintObject) { output?.log(`blueprint steps: ${options.blueprintObject.steps.length}`); @@ -156,7 +218,9 @@ export default async function startWPNow( return { php, phpInstances, + instanceMap, options, + refresher, }; } From e968403c22dda7fc078c44d75a9578bcb29be68c Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:46:38 -0400 Subject: [PATCH 02/56] Adding Pool class. --- packages/wp-now/src/pool.ts | 160 +++++++++++++++++++ packages/wp-now/src/start-server.ts | 34 +---- packages/wp-now/src/wp-now.ts | 228 +++++++++++++--------------- 3 files changed, 273 insertions(+), 149 deletions(-) create mode 100644 packages/wp-now/src/pool.ts diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts new file mode 100644 index 00000000..bbf3b5e6 --- /dev/null +++ b/packages/wp-now/src/pool.ts @@ -0,0 +1,160 @@ +import { NodePHP } from "@php-wasm/node"; + +const Spawn = Symbol('Spawn'); +const Reap = Symbol('Reap'); + +let childCount = 0; + +export class PoolInfo { + id = childCount++; + requests = 0; + started = Date.now(); + active = false; + busy = false; +} + +/** + * Maintains and refreshes a list of php instances + * such that each one will only be fed X number of requests + * before being discarded and replaced. + */ +export class Pool { + + instances = new Map; // php => PoolInfo + + spawner: () => Promise; // Callback to create new instances. + maxRequests: number; // Max requests to feed each instance + maxJobs: number; // Max number of instances to maintain at once. + + notifiers = new Map; // Inverted promises to notify async code of backlogged item processed. + running = new Set; // Set of busy PHP instances. + backlog = []; // Set of request callbacks waiting to be run. + + constructor({ + spawner = async (): Promise => {}, + maxRequests = 2000, + maxJobs = 5, + } = {}) { + Object.assign(this, {spawner, maxRequests, maxJobs}); + this[Reap](); + this[Spawn](); + } + + /** + * Find the next available idle instance. + */ + getIdleInstance() { + const sorted = [...this.instances].sort((a,b) => a[1].requests - b[1].requests); + + for (const [instance, info] of sorted) { + if(this.running.has(instance)) { + continue; + } + + if (!info.active) { + continue; + } + return instance; + } + + return false; + } + + /** + * Queue up a callback that will make a request when an + * instance becomes idle. + */ + async enqueue(item: (php: NodePHP) => Promise ) { + + const idleInstance = this.getIdleInstance(); + + if (!idleInstance) { // Defer the callback if we don't have an idle instance available. + + this.backlog.push(item); + + // Split a promise open so it can be resolved + // later when the item is processed. + const notifier = new Promise(accept => { + this.notifiers.set(item, accept); + }) + + return notifier; + + } else { // If we've got an instance available, run the provided callback. + + const info = this.instances.get(idleInstance); + + info.requests++; + + this.running.add(idleInstance); + + const wrapped = item(await idleInstance); + + const onCompleted = async () => { + + this.running.delete(idleInstance); + + this[Reap](); + this[Spawn](); + + if (!this.backlog.length) { + return; + } + + const idleInstanceNext = this.getIdleInstance(); + + const next = this.backlog.shift(); + const info = this.instances.get(idleInstanceNext); + + info.requests++; + + const wrapped = next(await idleInstanceNext); + + wrapped.finally(onCompleted); + + wrapped.then(ret => { + const notifier = this.notifiers.get(next); + console.log(notifier); + this.notifiers.delete(next); + notifier(ret); + }); + + this.running.add(idleInstance); + }; + + // When the provided callback completes, check to see if + // any more requests have been added to the pool + wrapped.finally(onCompleted); + + return wrapped; + } + } + + /** + * PRIVATE + * Spawns new instances if the pool is not full. + */ + [Spawn]() { + while (this.maxJobs > 0 && this.instances.size < this.maxJobs) { + const info = new PoolInfo; + const instance = this.spawner(); + this.instances.set(instance, info); + info.active = true; + } + } + + /** + * PRIVATE + * Reaps children if they've passed the maxRequest count. + */ + [Reap]() { + for (const [instance, info] of this.instances) { + + if (this.maxRequests > 0 && info.requests >= this.maxRequests) { + info.active = false; + this.instances.delete(instance); + continue; + } + } + } +} diff --git a/packages/wp-now/src/start-server.ts b/packages/wp-now/src/start-server.ts index 509d9179..6cba7b26 100644 --- a/packages/wp-now/src/start-server.ts +++ b/packages/wp-now/src/start-server.ts @@ -50,7 +50,7 @@ function shouldCompress(_, res) { } export async function startServer( - options: WPNowOptions = {numberOfPhpInstances: 5} + options: WPNowOptions = { numberOfPhpInstances: 1 } ): Promise { if (!fs.existsSync(options.projectPath)) { throw new Error( @@ -62,7 +62,7 @@ export async function startServer( app.use(compression({ filter: shouldCompress })); app.use(addTrailingSlash('/wp-admin')); const port = await portFinder.getOpenPort(); - const { php, instanceMap, options: wpNowOptions, refresher } = await startWPNow(options); + const {php, options: wpNowOptions, pool} = await startWPNow(options); app.use('/', async (req, res) => { try { @@ -104,33 +104,13 @@ export async function startServer( body: body as string, }; - await refresher(); - - const instanceMapSorted = [...instanceMap.entries()] - .sort((a, b) => a[1].requests - b[1].requests) + const resp = await pool.enqueue(php => php.request(data)); - for (const [php, info] of instanceMapSorted) { - if (!info.active || info.busy) { - continue; - } - - info.requests++; - info.busy = true; - - const resp = await php.request(data); - - info.busy = false; + res.statusCode = resp.httpStatusCode; - res.statusCode = resp.httpStatusCode; - - Object.keys(resp.headers).forEach((key) => { - res.setHeader(key, resp.headers[key]); - }); - - res.end(resp.bytes); - - break; - } + Object.keys(resp.headers).forEach((key) => res.setHeader(key, resp.headers[key])); + + res.end(resp.bytes); } catch (e) { output?.trace(e); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 2900ba5a..734cdcbd 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -32,6 +32,8 @@ import getWpNowPath from './get-wp-now-path'; import getWordpressVersionsPath from './get-wordpress-versions-path'; import getSqlitePath from './get-sqlite-path'; +import { Pool } from './pool'; + function seemsLikeAPHPFile(path) { return path.endsWith('.php') || path.includes('.php/'); } @@ -47,12 +49,11 @@ export default async function startWPNow( ): Promise<{ php: NodePHP; phpInstances: NodePHP[]; - instanceMap: Map; options: WPNowOptions; - refresher: Function; + pool: Pool; }> { const { documentRoot } = options; - + const nodePHPOptions: PHPLoaderOptions = { requestHandler: { documentRoot, @@ -72,34 +73,22 @@ export default async function startWPNow( }, }, }; + + const phpInstances = [await NodePHP.load(options.phpVersion, nodePHPOptions)]; + + const spawnInstance = async () => { + const php = await NodePHP.load(options.phpVersion, nodePHPOptions); + + php.mkdirTree(documentRoot); + php.chdir(documentRoot); + php.writeFile( + `${documentRoot}/index.php`, + `> => { - const newPHPs : Set = new Set; - // for (let i = 0; i < Math.max(options.numberOfPhpInstances, 1); i++) - while (instanceMap.size < options.numberOfPhpInstances) { - console.error('Respawning...'); - const php = await NodePHP.load(options.phpVersion, nodePHPOptions); - phpInstances.push(php); - instanceMap.set(php, {requests: 0, started: Date.now(), initialized: false, active: true, busy: false}); - newPHPs.add(php); - } - return newPHPs; - }; - - const clearInstances = async () => { - for (const [php, info] of instanceMap) { - if (info.requests > 20) { - info.active = false; - instanceMap.delete(php); - } - } + return php; }; - await fillInstances(); - const php = phpInstances[0]; phpInstances.forEach((_php) => { @@ -114,114 +103,109 @@ export default async function startWPNow( output?.log(`directory: ${options.projectPath}`); output?.log(`mode: ${options.mode}`); output?.log(`php: ${options.phpVersion}`); - + + const poolOptions = { + spawner: spawnInstance, + maxRequests: 1000, + maxJobs: 1, + maxIdle: -1, + }; + if (options.mode === WPNowMode.INDEX) { + const spawnAndSetup = async () => { + const instance = await spawnInstance(); + runIndexMode(instance, options); + return instance; + } - const refresher = async () => { - await clearInstances(); - const newPHPs = await fillInstances(); - await applyToInstances([...newPHPs.values()], async (_php) => { - instanceMap.get(php).initialized = true; - runIndexMode(_php, options); - }); - }; + poolOptions.spawner = spawnAndSetup; - await refresher(); - - return { php, phpInstances, instanceMap, options, refresher }; + const pool = new Pool(poolOptions); + + return {php, phpInstances, options, pool}; } + else + { + output?.log(`wp: ${options.wordPressVersion}`); - output?.log(`wp: ${options.wordPressVersion}`); + await Promise.all([ + downloadWordPress(options.wordPressVersion), + downloadSqliteIntegrationPlugin(), + downloadMuPlugins(), + ]); - await Promise.all([ - downloadWordPress(options.wordPressVersion), - downloadSqliteIntegrationPlugin(), - downloadMuPlugins(), - ]); - - if (options.reset) { - fs.removeSync(options.wpContentPath); - output?.log( - 'Created a fresh SQLite database and wp-content directory.' - ); - } + if (options.reset) { + fs.removeSync(options.wpContentPath); + output?.log( + 'Created a fresh SQLite database and wp-content directory.' + ); + } + + const setUpWordPress = async (_php) => { + switch (options.mode) { + case WPNowMode.WP_CONTENT: + await runWpContentMode(_php, options); + break; + case WPNowMode.WORDPRESS_DEVELOP: + await runWordPressDevelopMode(_php, options); + break; + case WPNowMode.WORDPRESS: + await runWordPressMode(_php, options); + break; + case WPNowMode.PLUGIN: + await runPluginOrThemeMode(_php, options); + break; + case WPNowMode.THEME: + await runPluginOrThemeMode(_php, options); + break; + case WPNowMode.PLAYGROUND: + await runWpPlaygroundMode(_php, options); + break; + } + }; - const isFirstTimeProject = !fs.existsSync(options.wpContentPath); - - const setUpWordPress = async (_php) => { - - const info = instanceMap.get(_php); - - info.initialized = true; - - switch (options.mode) { - case WPNowMode.WP_CONTENT: - await runWpContentMode(_php, options); - break; - case WPNowMode.WORDPRESS_DEVELOP: - await runWordPressDevelopMode(_php, options); - break; - case WPNowMode.WORDPRESS: - await runWordPressMode(_php, options); - break; - case WPNowMode.PLUGIN: - await runPluginOrThemeMode(_php, options); - break; - case WPNowMode.THEME: - await runPluginOrThemeMode(_php, options); - break; - case WPNowMode.PLAYGROUND: - await runWpPlaygroundMode(_php, options); - break; - } - }; + const spawnSetupAndLogin = async () => { + const instance = await spawnInstance(); + await setUpWordPress(instance); + await login(instance, {username: 'admin', password: 'password'}); + return instance; + } - const refresher = async () => { - await clearInstances(); - - const newPHPs = await fillInstances(); + poolOptions.spawner = spawnSetupAndLogin; - await applyToInstances([...newPHPs.values()], async _php => { - setUpWordPress(_php); - await login(_php, { - username: 'admin', - password: 'password', + const pool = new Pool(poolOptions); + + await applyToInstances(phpInstances, setUpWordPress); + + if (options.blueprintObject) { + output?.log(`blueprint steps: ${options.blueprintObject.steps.length}`); + const compiled = compileBlueprint(options.blueprintObject, { + onStepCompleted: (result, step: StepDefinition) => { + output?.log(`Blueprint step completed: ${step.step}`); + }, }); + await runBlueprintSteps(compiled, php); + } + + await installationStep2(php); + + await login(php, { + username: 'admin', + password: 'password', }); - }; - - await applyToInstances([...instanceMap.keys()], setUpWordPress); - - if (options.blueprintObject) { - output?.log(`blueprint steps: ${options.blueprintObject.steps.length}`); - const compiled = compileBlueprint(options.blueprintObject, { - onStepCompleted: (result, step: StepDefinition) => { - output?.log(`Blueprint step completed: ${step.step}`); - }, - }); - await runBlueprintSteps(compiled, php); - } - await installationStep2(php); - await login(php, { - username: 'admin', - password: 'password', - }); - - if ( - isFirstTimeProject && - [WPNowMode.PLUGIN, WPNowMode.THEME].includes(options.mode) - ) { - await activatePluginOrTheme(php, options); + const isFirstTimeProject = !fs.existsSync(options.wpContentPath); + + if ( + isFirstTimeProject && + [WPNowMode.PLUGIN, WPNowMode.THEME].includes(options.mode) + ) { + await activatePluginOrTheme(php, options); + } + + return {php, phpInstances, options, pool}; } - return { - php, - phpInstances, - instanceMap, - options, - refresher, - }; } async function runIndexMode( From b515f1385f2497b6337ee2a32944e7efc83d11f6 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:49:12 -0400 Subject: [PATCH 03/56] Reverting change. --- .../lib/components/php-runner/php-loader.ts | 9 +- packages/wp-now/public/with-node-version.js | 4 +- packages/wp-now/src/pool.ts | 93 +++++----- packages/wp-now/src/start-server.ts | 15 +- packages/wp-now/src/wp-now.ts | 159 +++++++++--------- tsconfig.base.json | 70 ++++---- 6 files changed, 176 insertions(+), 174 deletions(-) diff --git a/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts b/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts index ae060df4..414bacf7 100644 --- a/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts +++ b/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts @@ -1,8 +1,5 @@ import type { LoadingStatus } from '../../types'; -import { - consumeAPI, - spawnPHPWorkerThread, -} from '@php-wasm/web'; +import { consumeAPI, spawnPHPWorkerThread } from '@php-wasm/web'; import type { PHPClient } from '../../php-worker'; @@ -24,9 +21,7 @@ export class PHPLoader extends EventTarget { /** @ts-ignore */ '../../php-worker.ts?url&worker' ); - const worker = await spawnPHPWorkerThread( - workerScriptUrl - ); + const worker = await spawnPHPWorkerThread(workerScriptUrl); const php = consumeAPI(worker); php?.onDownloadProgress((e) => { const { loaded, total } = e.detail; diff --git a/packages/wp-now/public/with-node-version.js b/packages/wp-now/public/with-node-version.js index cd145b5e..9268c249 100644 --- a/packages/wp-now/public/with-node-version.js +++ b/packages/wp-now/public/with-node-version.js @@ -6,9 +6,7 @@ import fs from 'fs'; // Set the minimum required/supported version of node here. // Check if `--blueprint=` is passed in proccess.argv -const hasBlueprint = process.argv.some((arg) => - arg.startsWith('--blueprint=') -); +const hasBlueprint = process.argv.some((arg) => arg.startsWith('--blueprint=')); const minimum = { // `--blueprint=` requires node v20 diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index bbf3b5e6..ad641a76 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -1,16 +1,16 @@ -import { NodePHP } from "@php-wasm/node"; +import { NodePHP } from '@php-wasm/node'; const Spawn = Symbol('Spawn'); -const Reap = Symbol('Reap'); +const Reap = Symbol('Reap'); let childCount = 0; export class PoolInfo { - id = childCount++; + id = childCount++; requests = 0; - started = Date.now(); - active = false; - busy = false; + started = Date.now(); + active = false; + busy = false; } /** @@ -19,23 +19,22 @@ export class PoolInfo { * before being discarded and replaced. */ export class Pool { - - instances = new Map; // php => PoolInfo - - spawner: () => Promise; // Callback to create new instances. - maxRequests: number; // Max requests to feed each instance - maxJobs: number; // Max number of instances to maintain at once. - - notifiers = new Map; // Inverted promises to notify async code of backlogged item processed. - running = new Set; // Set of busy PHP instances. - backlog = []; // Set of request callbacks waiting to be run. - + instances = new Map(); // php => PoolInfo + + spawner: () => Promise; // Callback to create new instances. + maxRequests: number; // Max requests to feed each instance + maxJobs: number; // Max number of instances to maintain at once. + + notifiers = new Map(); // Inverted promises to notify async code of backlogged item processed. + running = new Set(); // Set of busy PHP instances. + backlog = []; // Set of request callbacks waiting to be run. + constructor({ - spawner = async (): Promise => {}, + spawner = async (): Promise => {}, maxRequests = 2000, - maxJobs = 5, + maxJobs = 5, } = {}) { - Object.assign(this, {spawner, maxRequests, maxJobs}); + Object.assign(this, { spawner, maxRequests, maxJobs }); this[Reap](); this[Spawn](); } @@ -44,13 +43,15 @@ export class Pool { * Find the next available idle instance. */ getIdleInstance() { - const sorted = [...this.instances].sort((a,b) => a[1].requests - b[1].requests); + const sorted = [...this.instances].sort( + (a, b) => a[1].requests - b[1].requests + ); for (const [instance, info] of sorted) { - if(this.running.has(instance)) { + if (this.running.has(instance)) { continue; } - + if (!info.active) { continue; } @@ -64,61 +65,60 @@ export class Pool { * Queue up a callback that will make a request when an * instance becomes idle. */ - async enqueue(item: (php: NodePHP) => Promise ) { - + async enqueue(item: (php: NodePHP) => Promise) { const idleInstance = this.getIdleInstance(); - - if (!idleInstance) { // Defer the callback if we don't have an idle instance available. - + + if (!idleInstance) { + // Defer the callback if we don't have an idle instance available. + this.backlog.push(item); // Split a promise open so it can be resolved // later when the item is processed. - const notifier = new Promise(accept => { + const notifier = new Promise((accept) => { this.notifiers.set(item, accept); - }) + }); return notifier; - - } else { // If we've got an instance available, run the provided callback. + } else { + // If we've got an instance available, run the provided callback. const info = this.instances.get(idleInstance); info.requests++; - + this.running.add(idleInstance); - + const wrapped = item(await idleInstance); const onCompleted = async () => { - this.running.delete(idleInstance); this[Reap](); this[Spawn](); - + if (!this.backlog.length) { return; } - + const idleInstanceNext = this.getIdleInstance(); - + const next = this.backlog.shift(); const info = this.instances.get(idleInstanceNext); info.requests++; - + const wrapped = next(await idleInstanceNext); - + wrapped.finally(onCompleted); - - wrapped.then(ret => { + + wrapped.then((ret) => { const notifier = this.notifiers.get(next); console.log(notifier); this.notifiers.delete(next); notifier(ret); }); - + this.running.add(idleInstance); }; @@ -129,27 +129,26 @@ export class Pool { return wrapped; } } - + /** * PRIVATE * Spawns new instances if the pool is not full. */ [Spawn]() { while (this.maxJobs > 0 && this.instances.size < this.maxJobs) { - const info = new PoolInfo; + const info = new PoolInfo(); const instance = this.spawner(); this.instances.set(instance, info); info.active = true; } } - + /** * PRIVATE * Reaps children if they've passed the maxRequest count. */ [Reap]() { for (const [instance, info] of this.instances) { - if (this.maxRequests > 0 && info.requests >= this.maxRequests) { info.active = false; this.instances.delete(instance); diff --git a/packages/wp-now/src/start-server.ts b/packages/wp-now/src/start-server.ts index 6cba7b26..f2684a09 100644 --- a/packages/wp-now/src/start-server.ts +++ b/packages/wp-now/src/start-server.ts @@ -62,7 +62,7 @@ export async function startServer( app.use(compression({ filter: shouldCompress })); app.use(addTrailingSlash('/wp-admin')); const port = await portFinder.getOpenPort(); - const {php, options: wpNowOptions, pool} = await startWPNow(options); + const { php, options: wpNowOptions, pool } = await startWPNow(options); app.use('/', async (req, res) => { try { @@ -104,14 +104,15 @@ export async function startServer( body: body as string, }; - const resp = await pool.enqueue(php => php.request(data)); - + const resp = await pool.enqueue((php) => php.request(data)); + res.statusCode = resp.httpStatusCode; - - Object.keys(resp.headers).forEach((key) => res.setHeader(key, resp.headers[key])); - - res.end(resp.bytes); + Object.keys(resp.headers).forEach((key) => + res.setHeader(key, resp.headers[key]) + ); + + res.end(resp.bytes); } catch (e) { output?.trace(e); } diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 734cdcbd..a7c5fc1b 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -73,12 +73,12 @@ export default async function startWPNow( }, }, }; - + const phpInstances = [await NodePHP.load(options.phpVersion, nodePHPOptions)]; - + const spawnInstance = async () => { const php = await NodePHP.load(options.phpVersion, nodePHPOptions); - + php.mkdirTree(documentRoot); php.chdir(documentRoot); php.writeFile( @@ -107,8 +107,8 @@ export default async function startWPNow( const poolOptions = { spawner: spawnInstance, maxRequests: 1000, - maxJobs: 1, - maxIdle: -1, + maxJobs: 1, + maxIdle: -1, }; if (options.mode === WPNowMode.INDEX) { @@ -116,96 +116,95 @@ export default async function startWPNow( const instance = await spawnInstance(); runIndexMode(instance, options); return instance; - } + }; poolOptions.spawner = spawnAndSetup; const pool = new Pool(poolOptions); - return {php, phpInstances, options, pool}; + return { php, phpInstances, options, pool }; } - else - { - output?.log(`wp: ${options.wordPressVersion}`); - await Promise.all([ - downloadWordPress(options.wordPressVersion), - downloadSqliteIntegrationPlugin(), - downloadMuPlugins(), - ]); - - if (options.reset) { - fs.removeSync(options.wpContentPath); - output?.log( - 'Created a fresh SQLite database and wp-content directory.' - ); - } - - const setUpWordPress = async (_php) => { - switch (options.mode) { - case WPNowMode.WP_CONTENT: - await runWpContentMode(_php, options); - break; - case WPNowMode.WORDPRESS_DEVELOP: - await runWordPressDevelopMode(_php, options); - break; - case WPNowMode.WORDPRESS: - await runWordPressMode(_php, options); - break; - case WPNowMode.PLUGIN: - await runPluginOrThemeMode(_php, options); - break; - case WPNowMode.THEME: - await runPluginOrThemeMode(_php, options); - break; - case WPNowMode.PLAYGROUND: - await runWpPlaygroundMode(_php, options); - break; - } - }; + output?.log(`wp: ${options.wordPressVersion}`); + + await Promise.all([ + downloadWordPress(options.wordPressVersion), + downloadSqliteIntegrationPlugin(), + downloadMuPlugins(), + ]); + + if (options.reset) { + fs.removeSync(options.wpContentPath); + output?.log( + 'Created a fresh SQLite database and wp-content directory.' + ); + } - const spawnSetupAndLogin = async () => { - const instance = await spawnInstance(); - await setUpWordPress(instance); - await login(instance, {username: 'admin', password: 'password'}); - return instance; + const setUpWordPress = async (_php) => { + switch (options.mode) { + case WPNowMode.WP_CONTENT: + await runWpContentMode(_php, options); + break; + case WPNowMode.WORDPRESS_DEVELOP: + await runWordPressDevelopMode(_php, options); + break; + case WPNowMode.WORDPRESS: + await runWordPressMode(_php, options); + break; + case WPNowMode.PLUGIN: + await runPluginOrThemeMode(_php, options); + break; + case WPNowMode.THEME: + await runPluginOrThemeMode(_php, options); + break; + case WPNowMode.PLAYGROUND: + await runWpPlaygroundMode(_php, options); + break; } + }; + + const spawnSetupAndLogin = async () => { + const instance = await spawnInstance(); + await setUpWordPress(instance); + await login(instance, { username: 'admin', password: 'password' }); + return instance; + }; - poolOptions.spawner = spawnSetupAndLogin; + poolOptions.spawner = spawnSetupAndLogin; - const pool = new Pool(poolOptions); - - await applyToInstances(phpInstances, setUpWordPress); - - if (options.blueprintObject) { - output?.log(`blueprint steps: ${options.blueprintObject.steps.length}`); - const compiled = compileBlueprint(options.blueprintObject, { - onStepCompleted: (result, step: StepDefinition) => { - output?.log(`Blueprint step completed: ${step.step}`); - }, - }); - await runBlueprintSteps(compiled, php); - } - - await installationStep2(php); - - await login(php, { - username: 'admin', - password: 'password', + const pool = new Pool(poolOptions); + + await applyToInstances(phpInstances, setUpWordPress); + + if (options.blueprintObject) { + output?.log( + `blueprint steps: ${options.blueprintObject.steps.length}` + ); + const compiled = compileBlueprint(options.blueprintObject, { + onStepCompleted: (result, step: StepDefinition) => { + output?.log(`Blueprint step completed: ${step.step}`); + }, }); + await runBlueprintSteps(compiled, php); + } - const isFirstTimeProject = !fs.existsSync(options.wpContentPath); - - if ( - isFirstTimeProject && - [WPNowMode.PLUGIN, WPNowMode.THEME].includes(options.mode) - ) { - await activatePluginOrTheme(php, options); - } - - return {php, phpInstances, options, pool}; + await installationStep2(php); + + await login(php, { + username: 'admin', + password: 'password', + }); + + const isFirstTimeProject = !fs.existsSync(options.wpContentPath); + + if ( + isFirstTimeProject && + [WPNowMode.PLUGIN, WPNowMode.THEME].includes(options.mode) + ) { + await activatePluginOrTheme(php, options); } + return { php, phpInstances, options, pool }; } async function runIndexMode( diff --git a/tsconfig.base.json b/tsconfig.base.json index 492f7b4c..66377494 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,32 +1,42 @@ { - "compileOnSave": false, - "compilerOptions": { - "rootDir": ".", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "esModuleInterop": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": true, - "resolveJsonModule": true, - "jsx": "react", - "target": "ES2021", - "module": "esnext", - "lib": ["ES2022", "dom"], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", - "paths": { - "@wp-now/wp-now": ["packages/wp-now/src/index.ts"], - "@wp-playground/interactive-code-block": [ - "packages/interactive-code-block/src/index.ts" - ], - "@wp-playground/vscode-extension": [ - "packages/vscode-extension/src/index.ts" - ], - "nx-extensions": ["packages/nx-extensions/src/index.ts"] - } - }, - "exclude": ["node_modules", "tmp"] + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "resolveJsonModule": true, + "jsx": "react", + "target": "ES2021", + "module": "esnext", + "lib": [ + "ES2022", + "dom" + ], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": ".", + "paths": { + "@wp-now/wp-now": [ + "packages/wp-now/src/index.ts" + ], + "@wp-playground/interactive-code-block": [ + "packages/interactive-code-block/src/index.ts" + ], + "@wp-playground/vscode-extension": [ + "packages/vscode-extension/src/index.ts" + ], + "nx-extensions": [ + "packages/nx-extensions/src/index.ts" + ] + } + }, + "exclude": [ + "node_modules", + "tmp" + ] } From 5c7b52d7d895b526e22406fdc1efc42cb2cca7c8 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:52:02 -0400 Subject: [PATCH 04/56] Removing console.log --- packages/wp-now/src/pool.ts | 1 - packages/wp-now/src/wp-now.ts | 10 ++--- tsconfig.base.json | 70 +++++++++++++++-------------------- 3 files changed, 35 insertions(+), 46 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index ad641a76..9088ad62 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -114,7 +114,6 @@ export class Pool { wrapped.then((ret) => { const notifier = this.notifiers.get(next); - console.log(notifier); this.notifiers.delete(next); notifier(ret); }); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index a7c5fc1b..5771d7cd 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -74,7 +74,9 @@ export default async function startWPNow( }, }; - const phpInstances = [await NodePHP.load(options.phpVersion, nodePHPOptions)]; + const phpInstances = [ + await NodePHP.load(options.phpVersion, nodePHPOptions), + ]; const spawnInstance = async () => { const php = await NodePHP.load(options.phpVersion, nodePHPOptions); @@ -124,7 +126,7 @@ export default async function startWPNow( return { php, phpInstances, options, pool }; } - + output?.log(`wp: ${options.wordPressVersion}`); await Promise.all([ @@ -177,9 +179,7 @@ export default async function startWPNow( await applyToInstances(phpInstances, setUpWordPress); if (options.blueprintObject) { - output?.log( - `blueprint steps: ${options.blueprintObject.steps.length}` - ); + output?.log(`blueprint steps: ${options.blueprintObject.steps.length}`); const compiled = compileBlueprint(options.blueprintObject, { onStepCompleted: (result, step: StepDefinition) => { output?.log(`Blueprint step completed: ${step.step}`); diff --git a/tsconfig.base.json b/tsconfig.base.json index 66377494..492f7b4c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,42 +1,32 @@ { - "compileOnSave": false, - "compilerOptions": { - "rootDir": ".", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "esModuleInterop": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": true, - "resolveJsonModule": true, - "jsx": "react", - "target": "ES2021", - "module": "esnext", - "lib": [ - "ES2022", - "dom" - ], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", - "paths": { - "@wp-now/wp-now": [ - "packages/wp-now/src/index.ts" - ], - "@wp-playground/interactive-code-block": [ - "packages/interactive-code-block/src/index.ts" - ], - "@wp-playground/vscode-extension": [ - "packages/vscode-extension/src/index.ts" - ], - "nx-extensions": [ - "packages/nx-extensions/src/index.ts" - ] - } - }, - "exclude": [ - "node_modules", - "tmp" - ] + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "resolveJsonModule": true, + "jsx": "react", + "target": "ES2021", + "module": "esnext", + "lib": ["ES2022", "dom"], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": ".", + "paths": { + "@wp-now/wp-now": ["packages/wp-now/src/index.ts"], + "@wp-playground/interactive-code-block": [ + "packages/interactive-code-block/src/index.ts" + ], + "@wp-playground/vscode-extension": [ + "packages/vscode-extension/src/index.ts" + ], + "nx-extensions": ["packages/nx-extensions/src/index.ts"] + } + }, + "exclude": ["node_modules", "tmp"] } From a33b0c512edb0ee8e8f1f76af561938f01f3a36a Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:22:59 -0400 Subject: [PATCH 05/56] Max request options. --- packages/wp-now/src/config.ts | 4 ++ packages/wp-now/src/execute-php.ts | 2 +- packages/wp-now/src/pool.ts | 85 +++++++++++++++++++----------- packages/wp-now/src/run-cli.ts | 5 ++ packages/wp-now/src/wp-now.ts | 7 ++- 5 files changed, 68 insertions(+), 35 deletions(-) diff --git a/packages/wp-now/src/config.ts b/packages/wp-now/src/config.ts index 76dd4f80..8187b14f 100644 --- a/packages/wp-now/src/config.ts +++ b/packages/wp-now/src/config.ts @@ -20,6 +20,7 @@ export interface CliOptions { port?: number; blueprint?: string; reset?: boolean; + maxRequests?: number; } export const enum WPNowMode { @@ -43,6 +44,7 @@ export interface WPNowOptions { wpContentPath?: string; wordPressVersion?: string; numberOfPhpInstances?: number; + maxRequests?: number; blueprintObject?: Blueprint; reset?: boolean; } @@ -54,6 +56,7 @@ export const DEFAULT_OPTIONS: WPNowOptions = { projectPath: process.cwd(), mode: WPNowMode.AUTO, numberOfPhpInstances: 1, + maxRequests: 512, reset: false, }; @@ -111,6 +114,7 @@ export default async function getWpNowConfig( phpVersion: args.php as SupportedPHPVersion, projectPath: args.path as string, wordPressVersion: args.wp as string, + maxRequests: args.maxRequests as number, port, reset: args.reset as boolean, }; diff --git a/packages/wp-now/src/execute-php.ts b/packages/wp-now/src/execute-php.ts index 9c647f71..aa2fd708 100644 --- a/packages/wp-now/src/execute-php.ts +++ b/packages/wp-now/src/execute-php.ts @@ -27,7 +27,7 @@ export async function executePHP( ...options, numberOfPhpInstances: 2, }); - const [, php] = phpInstances; + const [php] = phpInstances; try { php.useHostFilesystem(); diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 9088ad62..cdae3925 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -1,7 +1,8 @@ import { NodePHP } from '@php-wasm/node'; +const Fatal = Symbol('Fatal'); const Spawn = Symbol('Spawn'); -const Reap = Symbol('Reap'); +const Reap = Symbol('Reap'); let childCount = 0; @@ -10,7 +11,6 @@ export class PoolInfo { requests = 0; started = Date.now(); active = false; - busy = false; } /** @@ -70,27 +70,17 @@ export class Pool { if (!idleInstance) { // Defer the callback if we don't have an idle instance available. - this.backlog.push(item); - // Split a promise open so it can be resolved - // later when the item is processed. - const notifier = new Promise((accept) => { - this.notifiers.set(item, accept); - }); + // Split a promise open so it can be accepted or + // rejected later when the item is processed. + const notifier = new Promise((accept, reject) => this.notifiers.set(item, [accept, reject])); return notifier; - } else { - // If we've got an instance available, run the provided callback. - - const info = this.instances.get(idleInstance); - - info.requests++; - - this.running.add(idleInstance); - - const wrapped = item(await idleInstance); + } else { // If we've got an instance available, run the provided callback. + // When the provided callback completes, check to see if + // any more requests have been added to the pool const onCompleted = async () => { this.running.delete(idleInstance); @@ -107,25 +97,47 @@ export class Pool { const info = this.instances.get(idleInstanceNext); info.requests++; - - const wrapped = next(await idleInstanceNext); - - wrapped.finally(onCompleted); - - wrapped.then((ret) => { + + const request = next(await idleInstanceNext); + + request.finally(() => { + console.error(`Used PHP #${info.id} Requests: ${info.requests} / ${this.maxRequests}...`); + onCompleted(); + }); + + request.then(ret => { const notifier = this.notifiers.get(next); this.notifiers.delete(next); - notifier(ret); + notifier[0](ret); }); - + + request.catch(err => { + const notifier = this.notifiers.get(next); + this.notifiers.delete(next); + notifier[1](err); + this[Fatal](idleInstanceNext, error); + }); + this.running.add(idleInstance); }; - // When the provided callback completes, check to see if - // any more requests have been added to the pool - wrapped.finally(onCompleted); + const info = this.instances.get(idleInstance); + + info.requests++; + + this.running.add(idleInstance); + + const request = item(await idleInstance); + + request.catch(error => this[Fatal](idleInstance, error)); - return wrapped; + // Make sure onComplete runs no matter how the request resolves + request.finally(() => { + console.error(`Used PHP #${info.id} Requests: ${info.requests} / ${this.maxRequests}.`); + onCompleted(); + }); + + return request; } } @@ -155,4 +167,17 @@ export class Pool { } } } + + /** + * PRIVATE + * Handle fatal errors gracefully. + */ + [Fatal](instance, error) { + console.error(error); + if (this.instances.has(instance)) { + const info = this.instances.get(instance); + info.active = false; + this.instances.delete(instance); + } + } } diff --git a/packages/wp-now/src/run-cli.ts b/packages/wp-now/src/run-cli.ts index 1865e443..3c3c7712 100644 --- a/packages/wp-now/src/run-cli.ts +++ b/packages/wp-now/src/run-cli.ts @@ -76,6 +76,10 @@ export async function runCli() { 'Create a new project environment, destroying the old project environment.', type: 'boolean', }); + yargs.option("maxRequests", { + describe: "Max number of requests before refreshing PHP instance.", + type: "number" + }); }, async (argv) => { const spinner = startSpinner('Starting the server...'); @@ -87,6 +91,7 @@ export async function runCli() { port: argv.port as number, blueprint: argv.blueprint as string, reset: argv.reset as boolean, + maxRequests: argv.maxRequests as number, }); portFinder.setPort(options.port as number); const { url } = await startServer(options); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 5771d7cd..fcffa031 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -107,10 +107,9 @@ export default async function startWPNow( output?.log(`php: ${options.phpVersion}`); const poolOptions = { - spawner: spawnInstance, - maxRequests: 1000, - maxJobs: 1, - maxIdle: -1, + spawner: spawnInstance, + maxRequests: options.maxRequests ?? 512, + maxJobs: 1, }; if (options.mode === WPNowMode.INDEX) { From f8326cd88c3877c01df150266b6b78a3a8dda4c4 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 14 Sep 2023 16:32:16 -0400 Subject: [PATCH 06/56] Tweaks. --- packages/wp-now/src/pool.ts | 41 ++++++++++++++++------------------ packages/wp-now/src/run-cli.ts | 9 ++++---- packages/wp-now/src/wp-now.ts | 4 ++-- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index cdae3925..a540a7b9 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -2,7 +2,7 @@ import { NodePHP } from '@php-wasm/node'; const Fatal = Symbol('Fatal'); const Spawn = Symbol('Spawn'); -const Reap = Symbol('Reap'); +const Reap = Symbol('Reap'); let childCount = 0; @@ -72,12 +72,15 @@ export class Pool { // Defer the callback if we don't have an idle instance available. this.backlog.push(item); - // Split a promise open so it can be accepted or + // Split a promise open so it can be accepted or // rejected later when the item is processed. - const notifier = new Promise((accept, reject) => this.notifiers.set(item, [accept, reject])); + const notifier = new Promise((accept, reject) => + this.notifiers.set(item, [accept, reject]) + ); return notifier; - } else { // If we've got an instance available, run the provided callback. + } else { + // If we've got an instance available, run the provided callback. // When the provided callback completes, check to see if // any more requests have been added to the pool @@ -97,45 +100,39 @@ export class Pool { const info = this.instances.get(idleInstanceNext); info.requests++; - + const request = next(await idleInstanceNext); - - request.finally(() => { - console.error(`Used PHP #${info.id} Requests: ${info.requests} / ${this.maxRequests}...`); - onCompleted(); - }); - + + request.finally(onCompleted); + request.then(ret => { const notifier = this.notifiers.get(next); this.notifiers.delete(next); notifier[0](ret); }); - - request.catch(err => { + + request.catch(error => { const notifier = this.notifiers.get(next); this.notifiers.delete(next); - notifier[1](err); + notifier[1](error); this[Fatal](idleInstanceNext, error); - }); - + }); + this.running.add(idleInstance); }; const info = this.instances.get(idleInstance); info.requests++; - + this.running.add(idleInstance); const request = item(await idleInstance); - request.catch(error => this[Fatal](idleInstance, error)); + request.catch((error) => this[Fatal](idleInstance, error)); // Make sure onComplete runs no matter how the request resolves - request.finally(() => { - console.error(`Used PHP #${info.id} Requests: ${info.requests} / ${this.maxRequests}.`); - onCompleted(); - }); + request.finally(onCompleted); return request; } diff --git a/packages/wp-now/src/run-cli.ts b/packages/wp-now/src/run-cli.ts index 3c3c7712..b99d0728 100644 --- a/packages/wp-now/src/run-cli.ts +++ b/packages/wp-now/src/run-cli.ts @@ -76,10 +76,11 @@ export async function runCli() { 'Create a new project environment, destroying the old project environment.', type: 'boolean', }); - yargs.option("maxRequests", { - describe: "Max number of requests before refreshing PHP instance.", - type: "number" - }); + yargs.option('maxRequests', { + describe: + 'Max number of requests before refreshing PHP instance.', + type: 'number', + }); }, async (argv) => { const spinner = startSpinner('Starting the server...'); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index fcffa031..c170b286 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -107,9 +107,9 @@ export default async function startWPNow( output?.log(`php: ${options.phpVersion}`); const poolOptions = { - spawner: spawnInstance, + spawner: spawnInstance, maxRequests: options.maxRequests ?? 512, - maxJobs: 1, + maxJobs: 1, }; if (options.mode === WPNowMode.INDEX) { From cb383fe15448a72b4d37cb8d1b330e4837c320ba Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:05:52 -0400 Subject: [PATCH 07/56] Tweaks. --- .../src/lib/components/php-runner/php-loader.ts | 9 +++++++-- packages/wp-now/public/with-node-version.js | 4 +++- packages/wp-now/src/execute-php.ts | 2 +- packages/wp-now/src/pool.ts | 4 ++-- packages/wp-now/src/start-server.ts | 2 +- packages/wp-now/src/wp-now.ts | 10 ++++++---- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts b/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts index 414bacf7..ae060df4 100644 --- a/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts +++ b/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts @@ -1,5 +1,8 @@ import type { LoadingStatus } from '../../types'; -import { consumeAPI, spawnPHPWorkerThread } from '@php-wasm/web'; +import { + consumeAPI, + spawnPHPWorkerThread, +} from '@php-wasm/web'; import type { PHPClient } from '../../php-worker'; @@ -21,7 +24,9 @@ export class PHPLoader extends EventTarget { /** @ts-ignore */ '../../php-worker.ts?url&worker' ); - const worker = await spawnPHPWorkerThread(workerScriptUrl); + const worker = await spawnPHPWorkerThread( + workerScriptUrl + ); const php = consumeAPI(worker); php?.onDownloadProgress((e) => { const { loaded, total } = e.detail; diff --git a/packages/wp-now/public/with-node-version.js b/packages/wp-now/public/with-node-version.js index 9268c249..cd145b5e 100644 --- a/packages/wp-now/public/with-node-version.js +++ b/packages/wp-now/public/with-node-version.js @@ -6,7 +6,9 @@ import fs from 'fs'; // Set the minimum required/supported version of node here. // Check if `--blueprint=` is passed in proccess.argv -const hasBlueprint = process.argv.some((arg) => arg.startsWith('--blueprint=')); +const hasBlueprint = process.argv.some((arg) => + arg.startsWith('--blueprint=') +); const minimum = { // `--blueprint=` requires node v20 diff --git a/packages/wp-now/src/execute-php.ts b/packages/wp-now/src/execute-php.ts index aa2fd708..9c647f71 100644 --- a/packages/wp-now/src/execute-php.ts +++ b/packages/wp-now/src/execute-php.ts @@ -27,7 +27,7 @@ export async function executePHP( ...options, numberOfPhpInstances: 2, }); - const [php] = phpInstances; + const [, php] = phpInstances; try { php.useHostFilesystem(); diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index a540a7b9..bb3f2200 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -105,13 +105,13 @@ export class Pool { request.finally(onCompleted); - request.then(ret => { + request.then((ret) => { const notifier = this.notifiers.get(next); this.notifiers.delete(next); notifier[0](ret); }); - request.catch(error => { + request.catch((error) => { const notifier = this.notifiers.get(next); this.notifiers.delete(next); notifier[1](error); diff --git a/packages/wp-now/src/start-server.ts b/packages/wp-now/src/start-server.ts index f2684a09..cf2c9522 100644 --- a/packages/wp-now/src/start-server.ts +++ b/packages/wp-now/src/start-server.ts @@ -50,7 +50,7 @@ function shouldCompress(_, res) { } export async function startServer( - options: WPNowOptions = { numberOfPhpInstances: 1 } + options: WPNowOptions = {} ): Promise { if (!fs.existsSync(options.projectPath)) { throw new Error( diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index c170b286..1331e9b4 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -74,9 +74,11 @@ export default async function startWPNow( }, }; - const phpInstances = [ - await NodePHP.load(options.phpVersion, nodePHPOptions), - ]; + const phpInstances = []; + + while (phpInstances.length < options.numberOfPhpInstances) { + phpInstances.push(await NodePHP.load(options.phpVersion, nodePHPOptions)); + } const spawnInstance = async () => { const php = await NodePHP.load(options.phpVersion, nodePHPOptions); @@ -115,7 +117,7 @@ export default async function startWPNow( if (options.mode === WPNowMode.INDEX) { const spawnAndSetup = async () => { const instance = await spawnInstance(); - runIndexMode(instance, options); + await runIndexMode(instance, options); return instance; }; From ac4ea1b83af2684bda794fb1c84bf27e6cf2776c Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 14 Sep 2023 18:20:37 -0400 Subject: [PATCH 08/56] Initialize all instances --- packages/wp-now/src/wp-now.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 1331e9b4..4f87f7cc 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -77,7 +77,9 @@ export default async function startWPNow( const phpInstances = []; while (phpInstances.length < options.numberOfPhpInstances) { - phpInstances.push(await NodePHP.load(options.phpVersion, nodePHPOptions)); + phpInstances.push( + await NodePHP.load(options.phpVersion, nodePHPOptions) + ); } const spawnInstance = async () => { @@ -121,6 +123,8 @@ export default async function startWPNow( return instance; }; + await phpInstances.map(instance => runIndexMode(instance, options)); + poolOptions.spawner = spawnAndSetup; const pool = new Pool(poolOptions); From 0b9044f58f492083f058c301df44f523218d4ca2 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 14 Sep 2023 19:21:05 -0400 Subject: [PATCH 09/56] Tweak. --- packages/wp-now/src/pool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index bb3f2200..fec9bb5c 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -118,7 +118,7 @@ export class Pool { this[Fatal](idleInstanceNext, error); }); - this.running.add(idleInstance); + this.running.add(idleInstanceNext); }; const info = this.instances.get(idleInstance); From fc0f99507a64858000db2e709085e28ba458de16 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 14 Sep 2023 19:27:01 -0400 Subject: [PATCH 10/56] Tweak. --- .../src/lib/components/php-runner/php-loader.ts | 9 ++------- packages/wp-now/public/with-node-version.js | 4 +--- packages/wp-now/src/pool.ts | 2 ++ packages/wp-now/src/wp-now.ts | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts b/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts index ae060df4..414bacf7 100644 --- a/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts +++ b/packages/interactive-code-block/src/lib/components/php-runner/php-loader.ts @@ -1,8 +1,5 @@ import type { LoadingStatus } from '../../types'; -import { - consumeAPI, - spawnPHPWorkerThread, -} from '@php-wasm/web'; +import { consumeAPI, spawnPHPWorkerThread } from '@php-wasm/web'; import type { PHPClient } from '../../php-worker'; @@ -24,9 +21,7 @@ export class PHPLoader extends EventTarget { /** @ts-ignore */ '../../php-worker.ts?url&worker' ); - const worker = await spawnPHPWorkerThread( - workerScriptUrl - ); + const worker = await spawnPHPWorkerThread(workerScriptUrl); const php = consumeAPI(worker); php?.onDownloadProgress((e) => { const { loaded, total } = e.detail; diff --git a/packages/wp-now/public/with-node-version.js b/packages/wp-now/public/with-node-version.js index cd145b5e..9268c249 100644 --- a/packages/wp-now/public/with-node-version.js +++ b/packages/wp-now/public/with-node-version.js @@ -6,9 +6,7 @@ import fs from 'fs'; // Set the minimum required/supported version of node here. // Check if `--blueprint=` is passed in proccess.argv -const hasBlueprint = process.argv.some((arg) => - arg.startsWith('--blueprint=') -); +const hasBlueprint = process.argv.some((arg) => arg.startsWith('--blueprint=')); const minimum = { // `--blueprint=` requires node v20 diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index fec9bb5c..37387250 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -119,6 +119,8 @@ export class Pool { }); this.running.add(idleInstanceNext); + + request.finally(() => this.running.delete(idleInstanceNext)); }; const info = this.instances.get(idleInstance); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 4f87f7cc..619f75a2 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -123,7 +123,7 @@ export default async function startWPNow( return instance; }; - await phpInstances.map(instance => runIndexMode(instance, options)); + await phpInstances.map((instance) => runIndexMode(instance, options)); poolOptions.spawner = spawnAndSetup; From e18b1fd4d1b33ebba1789079db5462c1ce862872 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:02:17 -0400 Subject: [PATCH 11/56] Consolidating FINALLY block. --- packages/wp-now/src/pool.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 37387250..197685db 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -103,7 +103,10 @@ export class Pool { const request = next(await idleInstanceNext); - request.finally(onCompleted); + request.finally(() => { + this.running.delete(idleInstanceNext) + onCompleted(); + }); request.then((ret) => { const notifier = this.notifiers.get(next); @@ -119,8 +122,6 @@ export class Pool { }); this.running.add(idleInstanceNext); - - request.finally(() => this.running.delete(idleInstanceNext)); }; const info = this.instances.get(idleInstance); From 0ffaad7678767a3976c5c10a745a7e5488479dcc Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 15 Sep 2023 13:41:47 -0400 Subject: [PATCH 12/56] Cleaner pooling algorithm. --- packages/wp-now/src/pool.ts | 82 ++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 197685db..0af95d36 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -19,7 +19,7 @@ export class PoolInfo { * before being discarded and replaced. */ export class Pool { - instances = new Map(); // php => PoolInfo + instanceInfo = new Map(); // php => PoolInfo spawner: () => Promise; // Callback to create new instances. maxRequests: number; // Max requests to feed each instance @@ -43,7 +43,7 @@ export class Pool { * Find the next available idle instance. */ getIdleInstance() { - const sorted = [...this.instances].sort( + const sorted = [...this.instanceInfo].sort( (a, b) => a[1].requests - b[1].requests ); @@ -82,60 +82,81 @@ export class Pool { } else { // If we've got an instance available, run the provided callback. - // When the provided callback completes, check to see if - // any more requests have been added to the pool - const onCompleted = async () => { - this.running.delete(idleInstance); + // Given an instance, create a new callback that will clean up + // after the instance processes a request, and optionally + // will also kick off the next request. + const onCompleted = instance => async () => { + this.running.delete(instance); this[Reap](); - this[Spawn](); + const newInstances = this[Spawn](); if (!this.backlog.length) { return; } - const idleInstanceNext = this.getIdleInstance(); + // This is the instance that completed + // so we can re-use it... + let nextInstance = instance; + + // ... but, if we've just spanwed a fresh + // instance, use that one instead. + if (newInstances.size) + for(const instance of newInstances) { + nextInstance = instance; + break; + } const next = this.backlog.shift(); - const info = this.instances.get(idleInstanceNext); + const info = this.instanceInfo.get(nextInstance); + this.running.add(nextInstance); info.requests++; - const request = next(await idleInstanceNext); + const request = next(await nextInstance); + + const completed = onCompleted(nextInstance); + // Make sure onComplete & running.delete run + // no matter how the request resolves. request.finally(() => { - this.running.delete(idleInstanceNext) - onCompleted(); + this.running.delete(nextInstance) + completed(); }); + // Grab the accept handler from the notfier + // promise and run it if the request resolves. request.then((ret) => { const notifier = this.notifiers.get(next); this.notifiers.delete(next); notifier[0](ret); }); + // Grab the reject handler from the notfier + // promise and run it if the request rejects. request.catch((error) => { const notifier = this.notifiers.get(next); this.notifiers.delete(next); notifier[1](error); - this[Fatal](idleInstanceNext, error); + // Catch any errors and log to the console. + // Deactivate the instance. + this[Fatal](nextInstance, error); }); - - this.running.add(idleInstanceNext); }; - const info = this.instances.get(idleInstance); - - info.requests++; + const info = this.instanceInfo.get(idleInstance); this.running.add(idleInstance); + info.requests++; const request = item(await idleInstance); - request.catch((error) => this[Fatal](idleInstance, error)); + // Make sure onComplete runs no matter how the request resolves. + request.finally(onCompleted(idleInstance)); - // Make sure onComplete runs no matter how the request resolves - request.finally(onCompleted); + // Catch any errors and log to the console. + // Deactivate the instance. + request.catch((error) => this[Fatal](idleInstance, error)); return request; } @@ -144,14 +165,18 @@ export class Pool { /** * PRIVATE * Spawns new instances if the pool is not full. + * Returns a list of new instances. */ [Spawn]() { - while (this.maxJobs > 0 && this.instances.size < this.maxJobs) { + const newInstances = new Set; + while (this.maxJobs > 0 && this.instanceInfo.size < this.maxJobs) { const info = new PoolInfo(); const instance = this.spawner(); - this.instances.set(instance, info); + this.instanceInfo.set(instance, info); info.active = true; + newInstances.add(instance); } + return newInstances; } /** @@ -159,10 +184,11 @@ export class Pool { * Reaps children if they've passed the maxRequest count. */ [Reap]() { - for (const [instance, info] of this.instances) { + for (const [instance, info] of this.instanceInfo) { if (this.maxRequests > 0 && info.requests >= this.maxRequests) { info.active = false; - this.instances.delete(instance); + this.instanceInfo.delete(instance); + // instance.then(unwrapped => unwrapped.destroy()); continue; } } @@ -174,10 +200,10 @@ export class Pool { */ [Fatal](instance, error) { console.error(error); - if (this.instances.has(instance)) { - const info = this.instances.get(instance); + if (this.instanceInfo.has(instance)) { + const info = this.instanceInfo.get(instance); info.active = false; - this.instances.delete(instance); + this.instanceInfo.delete(instance); } } } From 3d26067afaddc107adf3dda8d24de9e5970c4769 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 15 Sep 2023 13:54:48 -0400 Subject: [PATCH 13/56] Comments --- packages/wp-now/src/pool.ts | 18 +++++----- tsconfig.base.json | 70 +++++++++++++++++++++---------------- 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 0af95d36..19078a7a 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -34,7 +34,9 @@ export class Pool { maxRequests = 2000, maxJobs = 5, } = {}) { - Object.assign(this, { spawner, maxRequests, maxJobs }); + this.spawner = spawner; + this.maxRequests = maxRequests; + this.maxJobs = maxJobs; this[Reap](); this[Spawn](); } @@ -85,7 +87,7 @@ export class Pool { // Given an instance, create a new callback that will clean up // after the instance processes a request, and optionally // will also kick off the next request. - const onCompleted = instance => async () => { + const onCompleted = (instance) => async () => { this.running.delete(instance); this[Reap](); @@ -102,10 +104,10 @@ export class Pool { // ... but, if we've just spanwed a fresh // instance, use that one instead. if (newInstances.size) - for(const instance of newInstances) { - nextInstance = instance; - break; - } + for (const instance of newInstances) { + nextInstance = instance; + break; + } const next = this.backlog.shift(); const info = this.instanceInfo.get(nextInstance); @@ -120,7 +122,7 @@ export class Pool { // Make sure onComplete & running.delete run // no matter how the request resolves. request.finally(() => { - this.running.delete(nextInstance) + this.running.delete(nextInstance); completed(); }); @@ -168,7 +170,7 @@ export class Pool { * Returns a list of new instances. */ [Spawn]() { - const newInstances = new Set; + const newInstances = new Set(); while (this.maxJobs > 0 && this.instanceInfo.size < this.maxJobs) { const info = new PoolInfo(); const instance = this.spawner(); diff --git a/tsconfig.base.json b/tsconfig.base.json index 492f7b4c..66377494 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,32 +1,42 @@ { - "compileOnSave": false, - "compilerOptions": { - "rootDir": ".", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "esModuleInterop": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": true, - "resolveJsonModule": true, - "jsx": "react", - "target": "ES2021", - "module": "esnext", - "lib": ["ES2022", "dom"], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", - "paths": { - "@wp-now/wp-now": ["packages/wp-now/src/index.ts"], - "@wp-playground/interactive-code-block": [ - "packages/interactive-code-block/src/index.ts" - ], - "@wp-playground/vscode-extension": [ - "packages/vscode-extension/src/index.ts" - ], - "nx-extensions": ["packages/nx-extensions/src/index.ts"] - } - }, - "exclude": ["node_modules", "tmp"] + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "resolveJsonModule": true, + "jsx": "react", + "target": "ES2021", + "module": "esnext", + "lib": [ + "ES2022", + "dom" + ], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": ".", + "paths": { + "@wp-now/wp-now": [ + "packages/wp-now/src/index.ts" + ], + "@wp-playground/interactive-code-block": [ + "packages/interactive-code-block/src/index.ts" + ], + "@wp-playground/vscode-extension": [ + "packages/vscode-extension/src/index.ts" + ], + "nx-extensions": [ + "packages/nx-extensions/src/index.ts" + ] + } + }, + "exclude": [ + "node_modules", + "tmp" + ] } From 909d08a7463146694aec16c0183d04dfd5cde4f0 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 15 Sep 2023 14:16:14 -0400 Subject: [PATCH 14/56] Comments. --- packages/wp-now/src/pool.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 19078a7a..7184895d 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -93,6 +93,7 @@ export class Pool { this[Reap](); const newInstances = this[Spawn](); + // Break out here if the backlog is empty. if (!this.backlog.length) { return; } From 1c91f34d7469dd292062ff630688dbeb73152eae Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 15 Sep 2023 14:37:56 -0400 Subject: [PATCH 15/56] Comments. --- packages/wp-now/src/pool.ts | 4 +++ tsconfig.base.json | 70 ++++++++++++++++--------------------- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 7184895d..6ea19e68 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -116,6 +116,8 @@ export class Pool { this.running.add(nextInstance); info.requests++; + // Don't ACTUALLY do anything until the + // instance is done spawning. const request = next(await nextInstance); const completed = onCompleted(nextInstance); @@ -152,6 +154,8 @@ export class Pool { this.running.add(idleInstance); info.requests++; + // Don't ACTUALLY do anything until the + // instance is done spawning. const request = item(await idleInstance); // Make sure onComplete runs no matter how the request resolves. diff --git a/tsconfig.base.json b/tsconfig.base.json index 66377494..492f7b4c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,42 +1,32 @@ { - "compileOnSave": false, - "compilerOptions": { - "rootDir": ".", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "esModuleInterop": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": true, - "resolveJsonModule": true, - "jsx": "react", - "target": "ES2021", - "module": "esnext", - "lib": [ - "ES2022", - "dom" - ], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", - "paths": { - "@wp-now/wp-now": [ - "packages/wp-now/src/index.ts" - ], - "@wp-playground/interactive-code-block": [ - "packages/interactive-code-block/src/index.ts" - ], - "@wp-playground/vscode-extension": [ - "packages/vscode-extension/src/index.ts" - ], - "nx-extensions": [ - "packages/nx-extensions/src/index.ts" - ] - } - }, - "exclude": [ - "node_modules", - "tmp" - ] + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "resolveJsonModule": true, + "jsx": "react", + "target": "ES2021", + "module": "esnext", + "lib": ["ES2022", "dom"], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": ".", + "paths": { + "@wp-now/wp-now": ["packages/wp-now/src/index.ts"], + "@wp-playground/interactive-code-block": [ + "packages/interactive-code-block/src/index.ts" + ], + "@wp-playground/vscode-extension": [ + "packages/vscode-extension/src/index.ts" + ], + "nx-extensions": ["packages/nx-extensions/src/index.ts"] + } + }, + "exclude": ["node_modules", "tmp"] } From a86c4c9c8f61e22ed9feabb04613ebaabc23432b Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:02:56 -0400 Subject: [PATCH 16/56] Catch spawn failures --- packages/wp-now/src/pool.ts | 65 +++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 6ea19e68..bafc1c16 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -30,7 +30,7 @@ export class Pool { backlog = []; // Set of request callbacks waiting to be run. constructor({ - spawner = async (): Promise => {}, + spawner = async (): Promise => {}, maxRequests = 2000, maxJobs = 5, } = {}) { @@ -77,9 +77,12 @@ export class Pool { // Split a promise open so it can be accepted or // rejected later when the item is processed. const notifier = new Promise((accept, reject) => - this.notifiers.set(item, [accept, reject]) + this.notifiers.set(item, {accept, reject}) ); + // Return the notifier so async calling code + // can still respond correctly when the item + // is finally processed. return notifier; } else { // If we've got an instance available, run the provided callback. @@ -116,9 +119,27 @@ export class Pool { this.running.add(nextInstance); info.requests++; - // Don't ACTUALLY do anything until the - // instance is done spawning. - const request = next(await nextInstance); + let request; + + try { + // Don't ACTUALLY do anything until the + // instance is done spawning. + request = next(await nextInstance); + } + catch(error) { + // Re-queue the request if the instance + // failed initialization. + this.backlog.unshift(next); + + // Catch any errors and log to the console. + // Deactivate the instance. + this[Fatal](nextInstance, error); + + // Return the notifier so async calling code + // can still respond correctly when the item + // is finally processed. + return this.notifiers.get(next); + } const completed = onCompleted(nextInstance); @@ -134,7 +155,7 @@ export class Pool { request.then((ret) => { const notifier = this.notifiers.get(next); this.notifiers.delete(next); - notifier[0](ret); + notifier.accept(ret); }); // Grab the reject handler from the notfier @@ -142,7 +163,7 @@ export class Pool { request.catch((error) => { const notifier = this.notifiers.get(next); this.notifiers.delete(next); - notifier[1](error); + notifier.reject(error); // Catch any errors and log to the console. // Deactivate the instance. this[Fatal](nextInstance, error); @@ -154,9 +175,33 @@ export class Pool { this.running.add(idleInstance); info.requests++; - // Don't ACTUALLY do anything until the - // instance is done spawning. - const request = item(await idleInstance); + let request; + + try { + // Don't ACTUALLY do anything until the + // instance is done spawning. + request = item(await idleInstance); + } + catch(error) { + // Re-queue the request if the instance + // failed initialization. + this.backlog.unshift(item); + + // Catch any errors and log to the console. + // Deactivate the instance. + this[Fatal](idleInstance, error); + + // Split a promise open so it can be accepted or + // rejected later when the item is processed. + const notifier = new Promise((accept, reject) => + this.notifiers.set(item, {accept, reject}) + ); + + // Return the notifier so async calling code + // can still respond correctly when the item + // is finally processed. + return notifier; + } // Make sure onComplete runs no matter how the request resolves. request.finally(onCompleted(idleInstance)); From 4b634bbace6ba365ee28eb8e158c545bd649062f Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:50:16 -0400 Subject: [PATCH 17/56] Making private methods privater. --- packages/wp-now/src/pool.ts | 196 ++++++++++++++++++------------------ 1 file changed, 100 insertions(+), 96 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index bafc1c16..d0df4b46 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -1,16 +1,87 @@ import { NodePHP } from '@php-wasm/node'; -const Fatal = Symbol('Fatal'); -const Spawn = Symbol('Spawn'); -const Reap = Symbol('Reap'); - let childCount = 0; -export class PoolInfo { - id = childCount++; - requests = 0; - started = Date.now(); - active = false; +/** + * PRIVATE + * Tracks stats of instances in a pool. + */ +class PoolInfo { + id = childCount++; // Unique ID for debugging purposes. + requests = 0; // Total requests processed. + started = Date.now(); // Time spawned. + active = false; // Whether instance is considered active. +} + +/** + * PRIVATE + * Spawns new instances if the pool is not full. + * Returns a list of new instances. + */ +const spawn = pool => { + const newInstances = new Set(); + + while (pool.maxJobs > 0 && pool.instanceInfo.size < pool.maxJobs) { + const info = new PoolInfo(); + const instance = pool.spawner(); + pool.instanceInfo.set(instance, info); + info.active = true; + newInstances.add(instance); + } + + return newInstances; +}; + +/** + * PRIVATE + * Reaps children if they've passed the maxRequest count. + */ +const reap = pool => { + for (const [instance, info] of pool.instanceInfo) { + if (pool.maxRequests > 0 && info.requests >= pool.maxRequests) { + info.active = false; + pool.instanceInfo.delete(instance); + continue; + } + } +}; + +/** + * PRIVATE + * Handle fatal errors gracefully. + */ +const fatal = (pool, instance, error) => { + + console.error(error); + + if (pool.instanceInfo.has(instance)) { + const info = pool.instanceInfo.get(instance); + info.active = false; + pool.instanceInfo.delete(instance); + } +}; + +/** + * PRIVATE + * Find the next available idle instance. + */ +const getIdleInstance = pool => { + const sorted = [...pool.instanceInfo].sort( + (a, b) => a[1].requests - b[1].requests + ); + + for (const [instance, info] of sorted) { + if (pool.running.has(instance)) { + continue; + } + + if (!info.active) { + continue; + } + return instance; + } + + return false; } /** @@ -30,37 +101,15 @@ export class Pool { backlog = []; // Set of request callbacks waiting to be run. constructor({ - spawner = async (): Promise => {}, + spawner = async (): Promise => {}, maxRequests = 2000, maxJobs = 5, } = {}) { this.spawner = spawner; this.maxRequests = maxRequests; this.maxJobs = maxJobs; - this[Reap](); - this[Spawn](); - } - - /** - * Find the next available idle instance. - */ - getIdleInstance() { - const sorted = [...this.instanceInfo].sort( - (a, b) => a[1].requests - b[1].requests - ); - - for (const [instance, info] of sorted) { - if (this.running.has(instance)) { - continue; - } - - if (!info.active) { - continue; - } - return instance; - } - - return false; + reap(this); + spawn(this); } /** @@ -68,7 +117,7 @@ export class Pool { * instance becomes idle. */ async enqueue(item: (php: NodePHP) => Promise) { - const idleInstance = this.getIdleInstance(); + const idleInstance = getIdleInstance(this); if (!idleInstance) { // Defer the callback if we don't have an idle instance available. @@ -77,7 +126,7 @@ export class Pool { // Split a promise open so it can be accepted or // rejected later when the item is processed. const notifier = new Promise((accept, reject) => - this.notifiers.set(item, {accept, reject}) + this.notifiers.set(item, { accept, reject }) ); // Return the notifier so async calling code @@ -93,8 +142,8 @@ export class Pool { const onCompleted = (instance) => async () => { this.running.delete(instance); - this[Reap](); - const newInstances = this[Spawn](); + reap(this); + const newInstances = spawn(this); // Break out here if the backlog is empty. if (!this.backlog.length) { @@ -108,10 +157,12 @@ export class Pool { // ... but, if we've just spanwed a fresh // instance, use that one instead. if (newInstances.size) + { for (const instance of newInstances) { nextInstance = instance; break; } + } const next = this.backlog.shift(); const info = this.instanceInfo.get(nextInstance); @@ -125,16 +176,15 @@ export class Pool { // Don't ACTUALLY do anything until the // instance is done spawning. request = next(await nextInstance); - } - catch(error) { + } catch (error) { // Re-queue the request if the instance // failed initialization. this.backlog.unshift(next); - + // Catch any errors and log to the console. // Deactivate the instance. - this[Fatal](nextInstance, error); - + fatal(this, nextInstance, error); + // Return the notifier so async calling code // can still respond correctly when the item // is finally processed. @@ -166,7 +216,7 @@ export class Pool { notifier.reject(error); // Catch any errors and log to the console. // Deactivate the instance. - this[Fatal](nextInstance, error); + fatal(this, nextInstance, error); }); }; @@ -181,20 +231,19 @@ export class Pool { // Don't ACTUALLY do anything until the // instance is done spawning. request = item(await idleInstance); - } - catch(error) { + } catch (error) { // Re-queue the request if the instance // failed initialization. this.backlog.unshift(item); - + // Catch any errors and log to the console. // Deactivate the instance. - this[Fatal](idleInstance, error); - + fatal(this, idleInstance, error); + // Split a promise open so it can be accepted or // rejected later when the item is processed. const notifier = new Promise((accept, reject) => - this.notifiers.set(item, {accept, reject}) + this.notifiers.set(item, { accept, reject }) ); // Return the notifier so async calling code @@ -208,54 +257,9 @@ export class Pool { // Catch any errors and log to the console. // Deactivate the instance. - request.catch((error) => this[Fatal](idleInstance, error)); + request.catch((error) => fatal(this, idleInstance, error)); return request; } } - - /** - * PRIVATE - * Spawns new instances if the pool is not full. - * Returns a list of new instances. - */ - [Spawn]() { - const newInstances = new Set(); - while (this.maxJobs > 0 && this.instanceInfo.size < this.maxJobs) { - const info = new PoolInfo(); - const instance = this.spawner(); - this.instanceInfo.set(instance, info); - info.active = true; - newInstances.add(instance); - } - return newInstances; - } - - /** - * PRIVATE - * Reaps children if they've passed the maxRequest count. - */ - [Reap]() { - for (const [instance, info] of this.instanceInfo) { - if (this.maxRequests > 0 && info.requests >= this.maxRequests) { - info.active = false; - this.instanceInfo.delete(instance); - // instance.then(unwrapped => unwrapped.destroy()); - continue; - } - } - } - - /** - * PRIVATE - * Handle fatal errors gracefully. - */ - [Fatal](instance, error) { - console.error(error); - if (this.instanceInfo.has(instance)) { - const info = this.instanceInfo.get(instance); - info.active = false; - this.instanceInfo.delete(instance); - } - } } From 3f7b796b821630872bbfe2a44a2541dacc3d6a02 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:57:39 -0400 Subject: [PATCH 18/56] Tweaks. --- packages/wp-now/src/pool.ts | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index d0df4b46..b0b14e68 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -18,9 +18,9 @@ class PoolInfo { * Spawns new instances if the pool is not full. * Returns a list of new instances. */ -const spawn = pool => { +const spawn = (pool) => { const newInstances = new Set(); - + while (pool.maxJobs > 0 && pool.instanceInfo.size < pool.maxJobs) { const info = new PoolInfo(); const instance = pool.spawner(); @@ -36,7 +36,7 @@ const spawn = pool => { * PRIVATE * Reaps children if they've passed the maxRequest count. */ -const reap = pool => { +const reap = (pool) => { for (const [instance, info] of pool.instanceInfo) { if (pool.maxRequests > 0 && info.requests >= pool.maxRequests) { info.active = false; @@ -51,7 +51,6 @@ const reap = pool => { * Handle fatal errors gracefully. */ const fatal = (pool, instance, error) => { - console.error(error); if (pool.instanceInfo.has(instance)) { @@ -65,7 +64,7 @@ const fatal = (pool, instance, error) => { * PRIVATE * Find the next available idle instance. */ -const getIdleInstance = pool => { +const getIdleInstance = (pool) => { const sorted = [...pool.instanceInfo].sort( (a, b) => a[1].requests - b[1].requests ); @@ -82,7 +81,7 @@ const getIdleInstance = pool => { } return false; -} +}; /** * Maintains and refreshes a list of php instances @@ -126,7 +125,7 @@ export class Pool { // Split a promise open so it can be accepted or // rejected later when the item is processed. const notifier = new Promise((accept, reject) => - this.notifiers.set(item, { accept, reject }) + this.notifiers.set(item, { accept, reject, notifier }) ); // Return the notifier so async calling code @@ -156,8 +155,7 @@ export class Pool { // ... but, if we've just spanwed a fresh // instance, use that one instead. - if (newInstances.size) - { + if (newInstances.size) { for (const instance of newInstances) { nextInstance = instance; break; @@ -185,10 +183,7 @@ export class Pool { // Deactivate the instance. fatal(this, nextInstance, error); - // Return the notifier so async calling code - // can still respond correctly when the item - // is finally processed. - return this.notifiers.get(next); + return; } const completed = onCompleted(nextInstance); From db3c5d6c7dc71979dc36f771a1203d32b5c87cde Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 15 Sep 2023 16:01:29 -0400 Subject: [PATCH 19/56] Tweaks. --- packages/wp-now/src/pool.ts | 2 +- tsconfig.base.json | 70 +++++++++++++++++++++---------------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index b0b14e68..a4f5252c 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -125,7 +125,7 @@ export class Pool { // Split a promise open so it can be accepted or // rejected later when the item is processed. const notifier = new Promise((accept, reject) => - this.notifiers.set(item, { accept, reject, notifier }) + this.notifiers.set(item, { accept, reject }) ); // Return the notifier so async calling code diff --git a/tsconfig.base.json b/tsconfig.base.json index 492f7b4c..66377494 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,32 +1,42 @@ { - "compileOnSave": false, - "compilerOptions": { - "rootDir": ".", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "esModuleInterop": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": true, - "resolveJsonModule": true, - "jsx": "react", - "target": "ES2021", - "module": "esnext", - "lib": ["ES2022", "dom"], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", - "paths": { - "@wp-now/wp-now": ["packages/wp-now/src/index.ts"], - "@wp-playground/interactive-code-block": [ - "packages/interactive-code-block/src/index.ts" - ], - "@wp-playground/vscode-extension": [ - "packages/vscode-extension/src/index.ts" - ], - "nx-extensions": ["packages/nx-extensions/src/index.ts"] - } - }, - "exclude": ["node_modules", "tmp"] + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "resolveJsonModule": true, + "jsx": "react", + "target": "ES2021", + "module": "esnext", + "lib": [ + "ES2022", + "dom" + ], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": ".", + "paths": { + "@wp-now/wp-now": [ + "packages/wp-now/src/index.ts" + ], + "@wp-playground/interactive-code-block": [ + "packages/interactive-code-block/src/index.ts" + ], + "@wp-playground/vscode-extension": [ + "packages/vscode-extension/src/index.ts" + ], + "nx-extensions": [ + "packages/nx-extensions/src/index.ts" + ] + } + }, + "exclude": [ + "node_modules", + "tmp" + ] } From 68f6290004ccda4b302894033dfe6f7a4756b5a1 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 15 Sep 2023 21:34:18 -0400 Subject: [PATCH 20/56] Correcting CLI switches, killing instances when done. --- .../src/executors/built-script/executor.ts | 6 ++++- .../src/executors/built-script/schema.d.ts | 6 ++++- .../src/executors/built-script/schema.json | 26 ++++++++++++++++--- packages/wp-now/src/pool.ts | 1 + packages/wp-now/src/run-cli.ts | 25 ++++++++++++++++++ packages/wp-now/src/wp-now.ts | 25 ++++++++++-------- 6 files changed, 73 insertions(+), 16 deletions(-) diff --git a/packages/nx-extensions/src/executors/built-script/executor.ts b/packages/nx-extensions/src/executors/built-script/executor.ts index 11024ef6..fb287845 100644 --- a/packages/nx-extensions/src/executors/built-script/executor.ts +++ b/packages/nx-extensions/src/executors/built-script/executor.ts @@ -7,7 +7,11 @@ const dirname = __dirname; export default async function runExecutor(options: BuiltScriptExecutorSchema) { const args = [ - ...(options.debug ? ['--inspect-brk'] : []), + ...(options['inspect'] ? ['--inspect-brk'] : []), + ...(options['inspect-brk'] ? ['--inspect-brk'] : []), + ...(options['trace-exit'] ? ['--trace-exit'] : []), + ...(options['trace-uncaught'] ? ['--trace-uncaught'] : []), + ...(options['trace-warnings'] ? ['--trace-warnings'] : []), '--loader', join(dirname, 'loader.mjs'), options.scriptPath, diff --git a/packages/nx-extensions/src/executors/built-script/schema.d.ts b/packages/nx-extensions/src/executors/built-script/schema.d.ts index 29aaa10c..33cd9159 100644 --- a/packages/nx-extensions/src/executors/built-script/schema.d.ts +++ b/packages/nx-extensions/src/executors/built-script/schema.d.ts @@ -1,5 +1,9 @@ export interface BuiltScriptExecutorSchema { scriptPath: string; - debug: boolean; + 'inspect': boolean; + 'inspect-brk': boolean; + 'trace-exit': boolean; + 'trace-uncaught': boolean; + 'trace-warnings': boolean; __unparsed__: string; } // eslint-disable-line diff --git a/packages/nx-extensions/src/executors/built-script/schema.json b/packages/nx-extensions/src/executors/built-script/schema.json index 35f44f4d..58ed1a55 100644 --- a/packages/nx-extensions/src/executors/built-script/schema.json +++ b/packages/nx-extensions/src/executors/built-script/schema.json @@ -10,10 +10,30 @@ "description": "Path of the script to run.", "x-prompt": "What script would you like to run?" }, - "debug": { + "inspect": { "type": "boolean", - "description": "Use devtools as a debugger.", - "x-prompt": "Would you like to use devtools?" + "description": "Use Node debugging client.", + "x-prompt": "Would you like to connect a Node debugging client?" + }, + "inspect-brk": { + "type": "boolean", + "description": "Use Node debugging client. Break immediately on script execution start.", + "x-prompt": "Would you like to connect a Node debugging client and break immediately on script execution start?" + }, + "trace-exit": { + "type": "boolean", + "description": "Prints a stack trace whenever an environment is exited proactively, i.e. invoking process.exit().", + "x-prompt": "Would you like print a stacktrace on exit?" + }, + "trace-uncaught": { + "type": "boolean", + "description": "Print stack traces for uncaught exceptions; usually, the stack trace associated with the creation of an Error is printed, whereas this makes Node.js also print the stack trace associated with throwing the value (which does not need to be an Error instance).", + "x-prompt": "Would you like print a stacktrace on uncaught promise rejection?" + }, + "trace-warnings": { + "type": "boolean", + "description": "Print stack traces for process warnings (including deprecations).", + "x-prompt": "Would you like print a stacktrace on warning?" }, "__unparsed__": { "hidden": true, diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index a4f5252c..582e6f7f 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -41,6 +41,7 @@ const reap = (pool) => { if (pool.maxRequests > 0 && info.requests >= pool.maxRequests) { info.active = false; pool.instanceInfo.delete(instance); + instance.then(unwrapped => unwrapped.exit()).catch(error => {}); continue; } } diff --git a/packages/wp-now/src/run-cli.ts b/packages/wp-now/src/run-cli.ts index b99d0728..306789ec 100644 --- a/packages/wp-now/src/run-cli.ts +++ b/packages/wp-now/src/run-cli.ts @@ -81,6 +81,31 @@ export async function runCli() { 'Max number of requests before refreshing PHP instance.', type: 'number', }); + yargs.option('inspect', { + describe: + 'Use Node debugging client.', + type: 'number', + }); + yargs.option('inspect-brk', { + describe: + 'Use Node debugging client. Break immediately on script execution start.', + type: 'number', + }); + yargs.option('trace-exit', { + describe: + 'Prints a stack trace whenever an environment is exited proactively, i.e. invoking process.exit().', + type: 'number', + }); + yargs.option('trace-uncaught', { + describe: + 'Print stack traces for uncaught exceptions; usually, the stack trace associated with the creation of an Error is printed, whereas this makes Node.js also print the stack trace associated with throwing the value (which does not need to be an Error instance).', + type: 'number', + }); + yargs.option('trace-warnings', { + describe: + 'Print stack traces for process warnings (including deprecations).', + type: 'number', + }); }, async (argv) => { const spinner = startSpinner('Starting the server...'); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 619f75a2..114ad3d9 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -170,17 +170,6 @@ export default async function startWPNow( } }; - const spawnSetupAndLogin = async () => { - const instance = await spawnInstance(); - await setUpWordPress(instance); - await login(instance, { username: 'admin', password: 'password' }); - return instance; - }; - - poolOptions.spawner = spawnSetupAndLogin; - - const pool = new Pool(poolOptions); - await applyToInstances(phpInstances, setUpWordPress); if (options.blueprintObject) { @@ -200,6 +189,20 @@ export default async function startWPNow( password: 'password', }); + const initialCookies = php.requestHandler.serializeCookies().split(';'); + + const spawnSetupAndLogin = async () => { + const instance = await spawnInstance(); + await setUpWordPress(instance); + instance.requestHandler.setCookies(initialCookies); + // await login(instance, { username: 'admin', password: 'password' }); + return instance; + }; + + poolOptions.spawner = spawnSetupAndLogin; + + const pool = new Pool(poolOptions); + const isFirstTimeProject = !fs.existsSync(options.wpContentPath); if ( From 9167e77c887e8056cb6a24abf276581467056f07 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Sat, 16 Sep 2023 01:54:41 -0400 Subject: [PATCH 21/56] Detect error in test. --- .../src/executors/built-script/schema.d.ts | 2 +- packages/wp-now/src/pool.ts | 2 +- packages/wp-now/src/run-cli.ts | 3 +- tsconfig.base.json | 70 ++++++++----------- 4 files changed, 33 insertions(+), 44 deletions(-) diff --git a/packages/nx-extensions/src/executors/built-script/schema.d.ts b/packages/nx-extensions/src/executors/built-script/schema.d.ts index 33cd9159..24aeacb2 100644 --- a/packages/nx-extensions/src/executors/built-script/schema.d.ts +++ b/packages/nx-extensions/src/executors/built-script/schema.d.ts @@ -1,6 +1,6 @@ export interface BuiltScriptExecutorSchema { scriptPath: string; - 'inspect': boolean; + inspect: boolean; 'inspect-brk': boolean; 'trace-exit': boolean; 'trace-uncaught': boolean; diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 582e6f7f..15a3e5cb 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -41,7 +41,7 @@ const reap = (pool) => { if (pool.maxRequests > 0 && info.requests >= pool.maxRequests) { info.active = false; pool.instanceInfo.delete(instance); - instance.then(unwrapped => unwrapped.exit()).catch(error => {}); + instance.then((unwrapped) => unwrapped.exit()).catch((error) => {}); continue; } } diff --git a/packages/wp-now/src/run-cli.ts b/packages/wp-now/src/run-cli.ts index 306789ec..abccc590 100644 --- a/packages/wp-now/src/run-cli.ts +++ b/packages/wp-now/src/run-cli.ts @@ -82,8 +82,7 @@ export async function runCli() { type: 'number', }); yargs.option('inspect', { - describe: - 'Use Node debugging client.', + describe: 'Use Node debugging client.', type: 'number', }); yargs.option('inspect-brk', { diff --git a/tsconfig.base.json b/tsconfig.base.json index 66377494..492f7b4c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,42 +1,32 @@ { - "compileOnSave": false, - "compilerOptions": { - "rootDir": ".", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "esModuleInterop": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": true, - "resolveJsonModule": true, - "jsx": "react", - "target": "ES2021", - "module": "esnext", - "lib": [ - "ES2022", - "dom" - ], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "baseUrl": ".", - "paths": { - "@wp-now/wp-now": [ - "packages/wp-now/src/index.ts" - ], - "@wp-playground/interactive-code-block": [ - "packages/interactive-code-block/src/index.ts" - ], - "@wp-playground/vscode-extension": [ - "packages/vscode-extension/src/index.ts" - ], - "nx-extensions": [ - "packages/nx-extensions/src/index.ts" - ] - } - }, - "exclude": [ - "node_modules", - "tmp" - ] + "compileOnSave": false, + "compilerOptions": { + "rootDir": ".", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "resolveJsonModule": true, + "jsx": "react", + "target": "ES2021", + "module": "esnext", + "lib": ["ES2022", "dom"], + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "baseUrl": ".", + "paths": { + "@wp-now/wp-now": ["packages/wp-now/src/index.ts"], + "@wp-playground/interactive-code-block": [ + "packages/interactive-code-block/src/index.ts" + ], + "@wp-playground/vscode-extension": [ + "packages/vscode-extension/src/index.ts" + ], + "nx-extensions": ["packages/nx-extensions/src/index.ts"] + } + }, + "exclude": ["node_modules", "tmp"] } From 70c3a22a29c6c6a91747eb9f19f079ae948f33ab Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Tue, 19 Sep 2023 08:35:01 -0400 Subject: [PATCH 22/56] Correcting multi-await --- packages/wp-now/src/start-server.ts | 2 ++ packages/wp-now/src/wp-now.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/wp-now/src/start-server.ts b/packages/wp-now/src/start-server.ts index cf2c9522..656bf545 100644 --- a/packages/wp-now/src/start-server.ts +++ b/packages/wp-now/src/start-server.ts @@ -28,9 +28,11 @@ function requestBodyToMultipartFormData(json, boundary) { const requestBodyToString = async (req) => await new Promise((resolve) => { let body = ''; + req.on('data', (chunk) => { body += chunk.toString(); // convert Buffer to string }); + req.on('end', () => { resolve(body); }); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 114ad3d9..6045e85b 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -123,7 +123,7 @@ export default async function startWPNow( return instance; }; - await phpInstances.map((instance) => runIndexMode(instance, options)); + await Promise.all(phpInstances.map((instance) => runIndexMode(instance, options))); poolOptions.spawner = spawnAndSetup; From aebc3d5a861216422bd81a8e6101997aff2da567 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Tue, 19 Sep 2023 08:35:07 -0400 Subject: [PATCH 23/56] Correcting multi-await --- packages/wp-now/src/start-server.ts | 4 ++-- packages/wp-now/src/wp-now.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/wp-now/src/start-server.ts b/packages/wp-now/src/start-server.ts index 656bf545..25a1b56e 100644 --- a/packages/wp-now/src/start-server.ts +++ b/packages/wp-now/src/start-server.ts @@ -28,11 +28,11 @@ function requestBodyToMultipartFormData(json, boundary) { const requestBodyToString = async (req) => await new Promise((resolve) => { let body = ''; - + req.on('data', (chunk) => { body += chunk.toString(); // convert Buffer to string }); - + req.on('end', () => { resolve(body); }); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 6045e85b..2a31289d 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -123,7 +123,9 @@ export default async function startWPNow( return instance; }; - await Promise.all(phpInstances.map((instance) => runIndexMode(instance, options))); + await Promise.all( + phpInstances.map((instance) => runIndexMode(instance, options)) + ); poolOptions.spawner = spawnAndSetup; From 96a2e8238d9e397be14dd0fc4558109e10f01d31 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Wed, 27 Sep 2023 07:41:25 -0400 Subject: [PATCH 24/56] Scaling back default maxRequests --- packages/wp-now/src/config.ts | 2 +- packages/wp-now/src/wp-now.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wp-now/src/config.ts b/packages/wp-now/src/config.ts index 8187b14f..560e995a 100644 --- a/packages/wp-now/src/config.ts +++ b/packages/wp-now/src/config.ts @@ -56,7 +56,7 @@ export const DEFAULT_OPTIONS: WPNowOptions = { projectPath: process.cwd(), mode: WPNowMode.AUTO, numberOfPhpInstances: 1, - maxRequests: 512, + maxRequests: 128, reset: false, }; diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 2a31289d..13214874 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -112,7 +112,7 @@ export default async function startWPNow( const poolOptions = { spawner: spawnInstance, - maxRequests: options.maxRequests ?? 512, + maxRequests: options.maxRequests ?? 128, maxJobs: 1, }; From 0cb98b797b6a520811b89beec080bef0a57481fe Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Tue, 3 Oct 2023 02:13:00 -0400 Subject: [PATCH 25/56] Pull request tweaks. --- packages/wp-now/src/config.ts | 4 + packages/wp-now/src/pool.ts | 210 +++++++++++++++++---------------- packages/wp-now/src/run-cli.ts | 6 + packages/wp-now/src/wp-now.ts | 2 +- 4 files changed, 117 insertions(+), 105 deletions(-) diff --git a/packages/wp-now/src/config.ts b/packages/wp-now/src/config.ts index 560e995a..02f3ba28 100644 --- a/packages/wp-now/src/config.ts +++ b/packages/wp-now/src/config.ts @@ -21,6 +21,7 @@ export interface CliOptions { blueprint?: string; reset?: boolean; maxRequests?: number; + maxJobs?: number; } export const enum WPNowMode { @@ -45,6 +46,7 @@ export interface WPNowOptions { wordPressVersion?: string; numberOfPhpInstances?: number; maxRequests?: number; + maxJobs?: number; blueprintObject?: Blueprint; reset?: boolean; } @@ -57,6 +59,7 @@ export const DEFAULT_OPTIONS: WPNowOptions = { mode: WPNowMode.AUTO, numberOfPhpInstances: 1, maxRequests: 128, + maxJobs: 1, reset: false, }; @@ -115,6 +118,7 @@ export default async function getWpNowConfig( projectPath: args.path as string, wordPressVersion: args.wp as string, maxRequests: args.maxRequests as number, + maxJobs: args.maxJobs as number, port, reset: args.reset as boolean, }; diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 15a3e5cb..1c36bb79 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -21,9 +21,11 @@ class PoolInfo { const spawn = (pool) => { const newInstances = new Set(); - while (pool.maxJobs > 0 && pool.instanceInfo.size < pool.maxJobs) { + if ( pool.maxJobs <= 0 ) return newInstances; + + while (pool.instanceInfo.size < pool.maxJobs) { const info = new PoolInfo(); - const instance = pool.spawner(); + const instance = pool.spawn(); pool.instanceInfo.set(instance, info); info.active = true; newInstances.add(instance); @@ -92,20 +94,20 @@ const getIdleInstance = (pool) => { export class Pool { instanceInfo = new Map(); // php => PoolInfo - spawner: () => Promise; // Callback to create new instances. + spawn: () => Promise; // Callback to create new instances. maxRequests: number; // Max requests to feed each instance maxJobs: number; // Max number of instances to maintain at once. - notifiers = new Map(); // Inverted promises to notify async code of backlogged item processed. + resolvers = new Map(); // Inverted promises to notify async code of backlogged item processed. running = new Set(); // Set of busy PHP instances. backlog = []; // Set of request callbacks waiting to be run. constructor({ spawner = async (): Promise => {}, - maxRequests = 2000, - maxJobs = 5, + maxRequests = 128, + maxJobs = 1, } = {}) { - this.spawner = spawner; + this.spawn = spawner; this.maxRequests = maxRequests; this.maxJobs = maxJobs; reap(this); @@ -126,99 +128,48 @@ export class Pool { // Split a promise open so it can be accepted or // rejected later when the item is processed. const notifier = new Promise((accept, reject) => - this.notifiers.set(item, { accept, reject }) + this.resolvers.set(item, { accept, reject }) ); // Return the notifier so async calling code // can still respond correctly when the item // is finally processed. return notifier; - } else { - // If we've got an instance available, run the provided callback. - - // Given an instance, create a new callback that will clean up - // after the instance processes a request, and optionally - // will also kick off the next request. - const onCompleted = (instance) => async () => { - this.running.delete(instance); - - reap(this); - const newInstances = spawn(this); - - // Break out here if the backlog is empty. - if (!this.backlog.length) { - return; - } - - // This is the instance that completed - // so we can re-use it... - let nextInstance = instance; - - // ... but, if we've just spanwed a fresh - // instance, use that one instead. - if (newInstances.size) { - for (const instance of newInstances) { - nextInstance = instance; - break; - } - } + } - const next = this.backlog.shift(); - const info = this.instanceInfo.get(nextInstance); + // If we've got an instance available, run the provided callback. - this.running.add(nextInstance); - info.requests++; + // Given an instance, create a new callback that will clean up + // after the instance processes a request, and optionally + // will also kick off the next request. + const onCompleted = (instance) => async () => { + this.running.delete(instance); - let request; + reap(this); + const newInstances = spawn(this); - try { - // Don't ACTUALLY do anything until the - // instance is done spawning. - request = next(await nextInstance); - } catch (error) { - // Re-queue the request if the instance - // failed initialization. - this.backlog.unshift(next); + // Break out here if the backlog is empty. + if (!this.backlog.length) { + return; + } - // Catch any errors and log to the console. - // Deactivate the instance. - fatal(this, nextInstance, error); + // This is the instance that completed + // so we can re-use it... + let nextInstance = instance; - return; + // ... but, if we've just spanwed a fresh + // instance, use that one instead. + if (newInstances.size) { + for (const instance of newInstances) { + nextInstance = instance; + break; } + } - const completed = onCompleted(nextInstance); - - // Make sure onComplete & running.delete run - // no matter how the request resolves. - request.finally(() => { - this.running.delete(nextInstance); - completed(); - }); - - // Grab the accept handler from the notfier - // promise and run it if the request resolves. - request.then((ret) => { - const notifier = this.notifiers.get(next); - this.notifiers.delete(next); - notifier.accept(ret); - }); - - // Grab the reject handler from the notfier - // promise and run it if the request rejects. - request.catch((error) => { - const notifier = this.notifiers.get(next); - this.notifiers.delete(next); - notifier.reject(error); - // Catch any errors and log to the console. - // Deactivate the instance. - fatal(this, nextInstance, error); - }); - }; - - const info = this.instanceInfo.get(idleInstance); - - this.running.add(idleInstance); + const next = this.backlog.shift(); + const info = this.instanceInfo.get(nextInstance); + + this.running.add(nextInstance); info.requests++; let request; @@ -226,36 +177,87 @@ export class Pool { try { // Don't ACTUALLY do anything until the // instance is done spawning. - request = item(await idleInstance); + request = next(await nextInstance); } catch (error) { // Re-queue the request if the instance // failed initialization. - this.backlog.unshift(item); + this.backlog.unshift(next); // Catch any errors and log to the console. // Deactivate the instance. - fatal(this, idleInstance, error); - - // Split a promise open so it can be accepted or - // rejected later when the item is processed. - const notifier = new Promise((accept, reject) => - this.notifiers.set(item, { accept, reject }) - ); - - // Return the notifier so async calling code - // can still respond correctly when the item - // is finally processed. - return notifier; + fatal(this, nextInstance, error); + + return; } - // Make sure onComplete runs no matter how the request resolves. - request.finally(onCompleted(idleInstance)); + const completed = onCompleted(nextInstance); + + // Make sure onComplete & running.delete run + // no matter how the request resolves. + request.finally(() => { + this.running.delete(nextInstance); + completed(); + }); + + // Grab the accept handler from the notfier + // promise and run it if the request resolves. + request.then((ret) => { + const notifier = this.resolvers.get(next); + this.resolvers.delete(next); + notifier.accept(ret); + }); + + // Grab the reject handler from the notfier + // promise and run it if the request rejects. + request.catch((error) => { + const notifier = this.resolvers.get(next); + this.resolvers.delete(next); + notifier.reject(error); + // Catch any errors and log to the console. + // Deactivate the instance. + fatal(this, nextInstance, error); + }); + }; + + const info = this.instanceInfo.get(idleInstance); + + this.running.add(idleInstance); + info.requests++; + + let request; + + try { + // Don't ACTUALLY do anything until the + // instance is done spawning. + request = item(await idleInstance); + } catch (error) { + // Re-queue the request if the instance + // failed initialization. + this.backlog.unshift(item); // Catch any errors and log to the console. // Deactivate the instance. - request.catch((error) => fatal(this, idleInstance, error)); + fatal(this, idleInstance, error); + + // Split a promise open so it can be accepted or + // rejected later when the item is processed. + const notifier = new Promise((accept, reject) => + this.resolvers.set(item, { accept, reject }) + ); - return request; + // Return the notifier so async calling code + // can still respond correctly when the item + // is finally processed. + return notifier; } + + // Make sure onComplete runs no matter how the request resolves. + request.finally(onCompleted(idleInstance)); + + // Catch any errors and log to the console. + // Deactivate the instance. + request.catch((error) => fatal(this, idleInstance, error)); + + return request; } } diff --git a/packages/wp-now/src/run-cli.ts b/packages/wp-now/src/run-cli.ts index abccc590..ad6bbfe4 100644 --- a/packages/wp-now/src/run-cli.ts +++ b/packages/wp-now/src/run-cli.ts @@ -81,6 +81,11 @@ export async function runCli() { 'Max number of requests before refreshing PHP instance.', type: 'number', }); + yargs.option('maxJobs', { + describe: + 'Max number of concurrent PHP instances.', + type: 'number', + }); yargs.option('inspect', { describe: 'Use Node debugging client.', type: 'number', @@ -117,6 +122,7 @@ export async function runCli() { blueprint: argv.blueprint as string, reset: argv.reset as boolean, maxRequests: argv.maxRequests as number, + maxJobs: argv.maxJobs as number, }); portFinder.setPort(options.port as number); const { url } = await startServer(options); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 13214874..dbe0618f 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -113,7 +113,7 @@ export default async function startWPNow( const poolOptions = { spawner: spawnInstance, maxRequests: options.maxRequests ?? 128, - maxJobs: 1, + maxJobs: options.maxJobs ?? 1, }; if (options.mode === WPNowMode.INDEX) { From a73874fdbe2d6f8d27abf3cf182ce90e48b77d3f Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 08:00:10 -0400 Subject: [PATCH 26/56] Doc Comments. --- packages/vscode-extension/project.json | 2 +- packages/wp-now/src/pool.ts | 70 ++++++++++++++++++-------- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/packages/vscode-extension/project.json b/packages/vscode-extension/project.json index 87569ded..22d56cbb 100644 --- a/packages/vscode-extension/project.json +++ b/packages/vscode-extension/project.json @@ -46,7 +46,7 @@ "cp ./LICENSE dist/packages/vscode-extension", "cp packages/vscode-extension/package.json dist/packages/vscode-extension", "cp packages/vscode-extension/README.md dist/packages/vscode-extension", - "cp node_modules/@php-wasm/node/*.wasm dist/packages/vscode-extension" + "find node_modules/@php-wasm/node/ -type f -name php_*.wasm | while read FILE; do cp $FILE dist/packages/vscode-extension; done" ], "parallel": false } diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 1c36bb79..6a3d66de 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -3,8 +3,8 @@ import { NodePHP } from '@php-wasm/node'; let childCount = 0; /** - * PRIVATE * Tracks stats of instances in a pool. + * @private */ class PoolInfo { id = childCount++; // Unique ID for debugging purposes. @@ -14,14 +14,15 @@ class PoolInfo { } /** - * PRIVATE * Spawns new instances if the pool is not full. * Returns a list of new instances. + * @param pool the pool object to work on + * @private */ -const spawn = (pool) => { +const spawn = (pool:Pool) => { const newInstances = new Set(); - if ( pool.maxJobs <= 0 ) return newInstances; + if (pool.maxJobs <= 0) return newInstances; while (pool.instanceInfo.size < pool.maxJobs) { const info = new PoolInfo(); @@ -35,25 +36,29 @@ const spawn = (pool) => { }; /** - * PRIVATE * Reaps children if they've passed the maxRequest count. + * @param pool the pool object to work on + * @private */ -const reap = (pool) => { +const reap = (pool:Pool) => { for (const [instance, info] of pool.instanceInfo) { if (pool.maxRequests > 0 && info.requests >= pool.maxRequests) { info.active = false; pool.instanceInfo.delete(instance); - instance.then((unwrapped) => unwrapped.exit()).catch((error) => {}); + instance.then((unwrapped) => unwrapped.exit()).catch(() => {}); continue; } } }; /** - * PRIVATE * Handle fatal errors gracefully. + * @param pool the pool object to work on + * @param instance the php instance to clean up + * @param error the actual error that got us here + * @private */ -const fatal = (pool, instance, error) => { +const fatal = (pool:Pool, instance:NodePHP, error:Error) => { console.error(error); if (pool.instanceInfo.has(instance)) { @@ -64,8 +69,8 @@ const fatal = (pool, instance, error) => { }; /** - * PRIVATE * Find the next available idle instance. + * @private */ const getIdleInstance = (pool) => { const sorted = [...pool.instanceInfo].sort( @@ -90,6 +95,23 @@ const getIdleInstance = (pool) => { * Maintains and refreshes a list of php instances * such that each one will only be fed X number of requests * before being discarded and replaced. + * + * Since we're dealing with a linear, "physical" memory array, as opposed to a + * virtual memory system afforded by most modern OSes, we're prone to things + * like memory fragmentation. In that situation, we could have the entire + * gigabyte empty except for a few sparse allocations. If no contiguous region + * of memory exists for the length requested, memory allocations will fail. + * This tends to happen when a new request attempts to initialize a heap + * structure but cannot find a contiguous 2mb chunk of memory. + * + * We can go as far as debugging PHP itself, and contributing the fix upstream. + * But even in this case we cannot guarantee that a third party extension will + * not introduce a leak sometime in the future. Therefore, we should have a + * solution robust to memory leaks that come from upstream code. I think that + * following the native strategy is the best way. + * + * https://www.php.net/manual/en/install.fpm.configuration.php#pm.max-requests + * */ export class Pool { instanceInfo = new Map(); // php => PoolInfo @@ -102,6 +124,10 @@ export class Pool { running = new Set(); // Set of busy PHP instances. backlog = []; // Set of request callbacks waiting to be run. + /** + * Create a new pool. + * @param options - {spawner, maxRequests, maxJobs} + */ constructor({ spawner = async (): Promise => {}, maxRequests = 128, @@ -117,6 +143,8 @@ export class Pool { /** * Queue up a callback that will make a request when an * instance becomes idle. + * @param item Callback to run when intance becomes available. Should accept the instance as the first and only param, and return a promise that resolves when the request is complete. + * @public */ async enqueue(item: (php: NodePHP) => Promise) { const idleInstance = getIdleInstance(this); @@ -125,10 +153,10 @@ export class Pool { // Defer the callback if we don't have an idle instance available. this.backlog.push(item); - // Split a promise open so it can be accepted or + // Split a promise open so it can be resolved or // rejected later when the item is processed. - const notifier = new Promise((accept, reject) => - this.resolvers.set(item, { accept, reject }) + const notifier = new Promise((resolve, reject) => + this.resolvers.set(item, { resolve, reject }) ); // Return the notifier so async calling code @@ -199,20 +227,20 @@ export class Pool { completed(); }); - // Grab the accept handler from the notfier + // Grab the resolve handler from the notfier // promise and run it if the request resolves. request.then((ret) => { - const notifier = this.resolvers.get(next); + const resolver = this.resolvers.get(next); this.resolvers.delete(next); - notifier.accept(ret); + resolver.resolve(ret); }); // Grab the reject handler from the notfier // promise and run it if the request rejects. request.catch((error) => { - const notifier = this.resolvers.get(next); + const resolver = this.resolvers.get(next); this.resolvers.delete(next); - notifier.reject(error); + resolver.reject(error); // Catch any errors and log to the console. // Deactivate the instance. fatal(this, nextInstance, error); @@ -239,10 +267,10 @@ export class Pool { // Deactivate the instance. fatal(this, idleInstance, error); - // Split a promise open so it can be accepted or + // Split a promise open so it can be resolved or // rejected later when the item is processed. - const notifier = new Promise((accept, reject) => - this.resolvers.set(item, { accept, reject }) + const notifier = new Promise((resolve, reject) => + this.resolvers.set(item, { resolve, reject }) ); // Return the notifier so async calling code From 326b4102fe2218bf16a7224fb29aff24d9a71126 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 08:00:23 -0400 Subject: [PATCH 27/56] Doc Comments. --- packages/wp-now/src/pool.ts | 6 +++--- packages/wp-now/src/run-cli.ts | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 6a3d66de..684d5060 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -19,7 +19,7 @@ class PoolInfo { * @param pool the pool object to work on * @private */ -const spawn = (pool:Pool) => { +const spawn = (pool: Pool) => { const newInstances = new Set(); if (pool.maxJobs <= 0) return newInstances; @@ -40,7 +40,7 @@ const spawn = (pool:Pool) => { * @param pool the pool object to work on * @private */ -const reap = (pool:Pool) => { +const reap = (pool: Pool) => { for (const [instance, info] of pool.instanceInfo) { if (pool.maxRequests > 0 && info.requests >= pool.maxRequests) { info.active = false; @@ -58,7 +58,7 @@ const reap = (pool:Pool) => { * @param error the actual error that got us here * @private */ -const fatal = (pool:Pool, instance:NodePHP, error:Error) => { +const fatal = (pool: Pool, instance: NodePHP, error: Error) => { console.error(error); if (pool.instanceInfo.has(instance)) { diff --git a/packages/wp-now/src/run-cli.ts b/packages/wp-now/src/run-cli.ts index ad6bbfe4..1397394b 100644 --- a/packages/wp-now/src/run-cli.ts +++ b/packages/wp-now/src/run-cli.ts @@ -82,8 +82,7 @@ export async function runCli() { type: 'number', }); yargs.option('maxJobs', { - describe: - 'Max number of concurrent PHP instances.', + describe: 'Max number of concurrent PHP instances.', type: 'number', }); yargs.option('inspect', { From 966a9dffe046e86d7c866f07421a8ca9b88f186f Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 09:12:52 -0400 Subject: [PATCH 28/56] Testing tweaks. --- packages/wp-now/src/wp-now.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index dbe0618f..05de305b 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -117,16 +117,16 @@ export default async function startWPNow( }; if (options.mode === WPNowMode.INDEX) { + await applyToInstances(phpInstances, async (_php:NodePHP) => { + runIndexMode(_php, options); + }); + const spawnAndSetup = async () => { const instance = await spawnInstance(); await runIndexMode(instance, options); return instance; }; - await Promise.all( - phpInstances.map((instance) => runIndexMode(instance, options)) - ); - poolOptions.spawner = spawnAndSetup; const pool = new Pool(poolOptions); @@ -149,7 +149,8 @@ export default async function startWPNow( ); } - const setUpWordPress = async (_php) => { + const isFirstTimeProject = !fs.existsSync(options.wpContentPath); + const setUpWordPress = async (_php:NodePHP) => { switch (options.mode) { case WPNowMode.WP_CONTENT: await runWpContentMode(_php, options); @@ -205,8 +206,6 @@ export default async function startWPNow( const pool = new Pool(poolOptions); - const isFirstTimeProject = !fs.existsSync(options.wpContentPath); - if ( isFirstTimeProject && [WPNowMode.PLUGIN, WPNowMode.THEME].includes(options.mode) From 488ad3caf58fb0d104f7a501282033d610ada636 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 09:27:18 -0400 Subject: [PATCH 29/56] Testing tweaks. --- packages/wp-now/src/tests/wp-now.spec.ts | 2 +- packages/wp-now/src/wp-now.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/wp-now/src/tests/wp-now.spec.ts b/packages/wp-now/src/tests/wp-now.spec.ts index af4fc469..f8e86fc2 100644 --- a/packages/wp-now/src/tests/wp-now.spec.ts +++ b/packages/wp-now/src/tests/wp-now.spec.ts @@ -859,4 +859,4 @@ describe('wp-cli command', () => { await executeWPCli(['cli', 'version']); expect(output).toMatch(/WP-CLI (\d\.?)+/i); }); -}); +}, 30_000); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 05de305b..6560fd11 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -76,7 +76,7 @@ export default async function startWPNow( const phpInstances = []; - while (phpInstances.length < options.numberOfPhpInstances) { + for (let i = 0; i < Math.max(options.numberOfPhpInstances, 1); i++) { phpInstances.push( await NodePHP.load(options.phpVersion, nodePHPOptions) ); @@ -117,7 +117,7 @@ export default async function startWPNow( }; if (options.mode === WPNowMode.INDEX) { - await applyToInstances(phpInstances, async (_php:NodePHP) => { + await applyToInstances(phpInstances, async (_php: NodePHP) => { runIndexMode(_php, options); }); @@ -150,7 +150,7 @@ export default async function startWPNow( } const isFirstTimeProject = !fs.existsSync(options.wpContentPath); - const setUpWordPress = async (_php:NodePHP) => { + const setUpWordPress = async (_php: NodePHP) => { switch (options.mode) { case WPNowMode.WP_CONTENT: await runWpContentMode(_php, options); @@ -204,8 +204,6 @@ export default async function startWPNow( poolOptions.spawner = spawnSetupAndLogin; - const pool = new Pool(poolOptions); - if ( isFirstTimeProject && [WPNowMode.PLUGIN, WPNowMode.THEME].includes(options.mode) @@ -213,6 +211,8 @@ export default async function startWPNow( await activatePluginOrTheme(php, options); } + const pool = new Pool(poolOptions); + return { php, phpInstances, options, pool }; } From a7708af6e88053147b8f04257797e60585fb0935 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 09:50:07 -0400 Subject: [PATCH 30/56] Testing tweaks. --- packages/wp-now/src/pool.ts | 14 +++++++------- packages/wp-now/src/wp-now.ts | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 684d5060..4d39ca80 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -19,14 +19,14 @@ class PoolInfo { * @param pool the pool object to work on * @private */ -const spawn = (pool: Pool) => { +const spawn = async (pool: Pool) => { const newInstances = new Set(); if (pool.maxJobs <= 0) return newInstances; while (pool.instanceInfo.size < pool.maxJobs) { const info = new PoolInfo(); - const instance = pool.spawn(); + const instance = await pool.spawn(); pool.instanceInfo.set(instance, info); info.active = true; newInstances.add(instance); @@ -45,7 +45,7 @@ const reap = (pool: Pool) => { if (pool.maxRequests > 0 && info.requests >= pool.maxRequests) { info.active = false; pool.instanceInfo.delete(instance); - instance.then((unwrapped) => unwrapped.exit()).catch(() => {}); + instance.exit().catch(() => {}); continue; } } @@ -116,7 +116,7 @@ const getIdleInstance = (pool) => { export class Pool { instanceInfo = new Map(); // php => PoolInfo - spawn: () => Promise; // Callback to create new instances. + spawn: () => Promise; // Async callback to create new instances. maxRequests: number; // Max requests to feed each instance maxJobs: number; // Max number of instances to maintain at once. @@ -174,7 +174,7 @@ export class Pool { this.running.delete(instance); reap(this); - const newInstances = spawn(this); + const newInstances = await spawn(this); // Break out here if the backlog is empty. if (!this.backlog.length) { @@ -205,7 +205,7 @@ export class Pool { try { // Don't ACTUALLY do anything until the // instance is done spawning. - request = next(await nextInstance); + request = next(nextInstance); } catch (error) { // Re-queue the request if the instance // failed initialization. @@ -257,7 +257,7 @@ export class Pool { try { // Don't ACTUALLY do anything until the // instance is done spawning. - request = item(await idleInstance); + request = item(idleInstance); } catch (error) { // Re-queue the request if the instance // failed initialization. diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 6560fd11..a1b07eed 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -198,7 +198,6 @@ export default async function startWPNow( const instance = await spawnInstance(); await setUpWordPress(instance); instance.requestHandler.setCookies(initialCookies); - // await login(instance, { username: 'admin', password: 'password' }); return instance; }; From e01d6d49857ecebe75e8d7eb12e56ea06796fcff Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:17:51 -0400 Subject: [PATCH 31/56] All tests pass. --- packages/wp-now/src/pool.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 4d39ca80..a62d5d93 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -45,7 +45,8 @@ const reap = (pool: Pool) => { if (pool.maxRequests > 0 && info.requests >= pool.maxRequests) { info.active = false; pool.instanceInfo.delete(instance); - instance.exit().catch(() => {}); + try { instance.exit(); } + catch {} continue; } } @@ -136,8 +137,6 @@ export class Pool { this.spawn = spawner; this.maxRequests = maxRequests; this.maxJobs = maxJobs; - reap(this); - spawn(this); } /** @@ -147,6 +146,9 @@ export class Pool { * @public */ async enqueue(item: (php: NodePHP) => Promise) { + reap(this); + await spawn(this); + const idleInstance = getIdleInstance(this); if (!idleInstance) { From eca59957a40237a628152a06bb70c3c708e59b2c Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:29:22 -0400 Subject: [PATCH 32/56] Passing linter --- packages/wp-now/src/pool.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index a62d5d93..19704c6e 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -45,8 +45,11 @@ const reap = (pool: Pool) => { if (pool.maxRequests > 0 && info.requests >= pool.maxRequests) { info.active = false; pool.instanceInfo.delete(instance); - try { instance.exit(); } - catch {} + try { + instance.exit(); + } catch { + (() => {})(); + } continue; } } From 3d8a1c29b58e3b17553f282e7756b450f6cda1f1 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:31:23 -0400 Subject: [PATCH 33/56] Passing linter --- packages/wp-now/src/pool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 19704c6e..54f2bb5b 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -48,7 +48,7 @@ const reap = (pool: Pool) => { try { instance.exit(); } catch { - (() => {})(); + () => void 0; } continue; } From 271c93596648c0fb80d0d3a53351255303e033fa Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:33:09 -0400 Subject: [PATCH 34/56] Passing linter --- packages/wp-now/src/pool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 54f2bb5b..85a3d351 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -48,7 +48,7 @@ const reap = (pool: Pool) => { try { instance.exit(); } catch { - () => void 0; + void 0; } continue; } From a7581dc909b8c43ac6690e517a8c7f212c2def7a Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:33:54 -0400 Subject: [PATCH 35/56] Temporarily skip typecheck --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83676297..88f9b5d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v3 - uses: ./.github/actions/prepare-playground - run: npx nx affected --target=lint - - run: npx nx affected --target=typecheck + # - run: npx nx affected --target=typecheck test: runs-on: ubuntu-latest needs: [lint-and-typecheck] From 8d968d7065ea357217ceab4cbc5ba7a123abfc9e Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:35:26 -0400 Subject: [PATCH 36/56] Revert temp change. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88f9b5d4..83676297 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v3 - uses: ./.github/actions/prepare-playground - run: npx nx affected --target=lint - # - run: npx nx affected --target=typecheck + - run: npx nx affected --target=typecheck test: runs-on: ubuntu-latest needs: [lint-and-typecheck] From 04a3e5eeabc24ab017b71e1fb0ef805daccbdced Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:23:34 -0400 Subject: [PATCH 37/56] Send 500 error to browser instead of re-queueing request. --- packages/wp-now/src/pool.ts | 87 +++++++++++++---------------- packages/wp-now/src/start-server.ts | 9 ++- packages/wp-now/src/wp-now.ts | 25 +++++++-- 3 files changed, 66 insertions(+), 55 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 85a3d351..da0a5760 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -1,4 +1,14 @@ -import { NodePHP } from '@php-wasm/node'; +type instance = any; + +type request = (instance: instance) => Promise; + +export type poolOptions = { + spawn: () => Promise, + reap: (instance:instance) => void, + fatal: (instance:instance, error:any) => void, + maxRequests: number + maxJobs: number +}; let childCount = 0; @@ -45,11 +55,7 @@ const reap = (pool: Pool) => { if (pool.maxRequests > 0 && info.requests >= pool.maxRequests) { info.active = false; pool.instanceInfo.delete(instance); - try { - instance.exit(); - } catch { - void 0; - } + pool.reap(instance); continue; } } @@ -62,7 +68,7 @@ const reap = (pool: Pool) => { * @param error the actual error that got us here * @private */ -const fatal = (pool: Pool, instance: NodePHP, error: Error) => { +const fatal = (pool: Pool, instance: instance, error: Error) => { console.error(error); if (pool.instanceInfo.has(instance)) { @@ -70,6 +76,8 @@ const fatal = (pool: Pool, instance: NodePHP, error: Error) => { info.active = false; pool.instanceInfo.delete(instance); } + + return pool.fatal(instance, error); }; /** @@ -121,6 +129,8 @@ export class Pool { instanceInfo = new Map(); // php => PoolInfo spawn: () => Promise; // Async callback to create new instances. + fatal: (instance: instance, error: any) => any; // Async callback called on instance fatal errors. + reap: (instance: instance) => void; // Async callback called on destroyed instances. maxRequests: number; // Max requests to feed each instance maxJobs: number; // Max number of instances to maintain at once. @@ -130,16 +140,22 @@ export class Pool { /** * Create a new pool. - * @param options - {spawner, maxRequests, maxJobs} + * @param options - {spawn, maxRequests, maxJobs} */ constructor({ - spawner = async (): Promise => {}, maxRequests = 128, maxJobs = 1, + spawn = async (): Promise => {}, + fatal = (instance: instance, request: request) => {}, + reap = (instance: instance) => {}, } = {}) { - this.spawn = spawner; - this.maxRequests = maxRequests; - this.maxJobs = maxJobs; + Object.defineProperties(this, { + maxRequests: {value: maxRequests}, + maxJobs: {value: maxJobs}, + spawn: {value: spawn}, + fatal: {value: fatal}, + reap: {value: reap}, + }); } /** @@ -148,7 +164,7 @@ export class Pool { * @param item Callback to run when intance becomes available. Should accept the instance as the first and only param, and return a promise that resolves when the request is complete. * @public */ - async enqueue(item: (php: NodePHP) => Promise) { + async enqueue(item: request): Promise { reap(this); await spawn(this); @@ -170,8 +186,6 @@ export class Pool { return notifier; } - // If we've got an instance available, run the provided callback. - // Given an instance, create a new callback that will clean up // after the instance processes a request, and optionally // will also kick off the next request. @@ -208,18 +222,13 @@ export class Pool { let request; try { - // Don't ACTUALLY do anything until the - // instance is done spawning. request = next(nextInstance); } catch (error) { - // Re-queue the request if the instance - // failed initialization. - this.backlog.unshift(next); - - // Catch any errors and log to the console. - // Deactivate the instance. - fatal(this, nextInstance, error); - + // Grab the reject handler from the notfier + // promise and run it if there is an error. + const resolver = this.resolvers.get(next); + this.resolvers.delete(next); + resolver.reject(fatal(this, nextInstance, error)); return; } @@ -245,10 +254,7 @@ export class Pool { request.catch((error) => { const resolver = this.resolvers.get(next); this.resolvers.delete(next); - resolver.reject(error); - // Catch any errors and log to the console. - // Deactivate the instance. - fatal(this, nextInstance, error); + resolver.reject(fatal(this, nextInstance, error)); }); }; @@ -259,29 +265,12 @@ export class Pool { let request; + // If we've got an instance available, run the provided callback. + try { - // Don't ACTUALLY do anything until the - // instance is done spawning. request = item(idleInstance); } catch (error) { - // Re-queue the request if the instance - // failed initialization. - this.backlog.unshift(item); - - // Catch any errors and log to the console. - // Deactivate the instance. - fatal(this, idleInstance, error); - - // Split a promise open so it can be resolved or - // rejected later when the item is processed. - const notifier = new Promise((resolve, reject) => - this.resolvers.set(item, { resolve, reject }) - ); - - // Return the notifier so async calling code - // can still respond correctly when the item - // is finally processed. - return notifier; + return Promise.reject(fatal(this, idleInstance, error)); } // Make sure onComplete runs no matter how the request resolves. diff --git a/packages/wp-now/src/start-server.ts b/packages/wp-now/src/start-server.ts index 25a1b56e..04ada0ca 100644 --- a/packages/wp-now/src/start-server.ts +++ b/packages/wp-now/src/start-server.ts @@ -106,7 +106,14 @@ export async function startServer( body: body as string, }; - const resp = await pool.enqueue((php) => php.request(data)); + let resp; + + try { + resp = await pool.enqueue((php) => php.request(data)); + } + catch (error) { + resp = error; + } res.statusCode = resp.httpStatusCode; diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index a1b07eed..80b149a3 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -32,7 +32,8 @@ import getWpNowPath from './get-wp-now-path'; import getWordpressVersionsPath from './get-wordpress-versions-path'; import getSqlitePath from './get-sqlite-path'; -import { Pool } from './pool'; +import { Pool, poolOptions } from './pool'; +import { PHPResponse } from '@php-wasm/universal'; function seemsLikeAPHPFile(path) { return path.endsWith('.php') || path.includes('.php/'); @@ -110,8 +111,22 @@ export default async function startWPNow( output?.log(`mode: ${options.mode}`); output?.log(`php: ${options.phpVersion}`); - const poolOptions = { - spawner: spawnInstance, + const poolOptions: poolOptions = { + spawn: spawnInstance, + reap: (instance:NodePHP) => { + try { + instance.exit(); + } catch { + void 0; + } + }, + fatal: (instance:NodePHP, error:any) => new PHPResponse( + 500, + {}, + new TextEncoder().encode( + `500 Internal Server Error:\n\n${String((error && error.stack) ? error.stack : error)}` + ) + ), maxRequests: options.maxRequests ?? 128, maxJobs: options.maxJobs ?? 1, }; @@ -127,7 +142,7 @@ export default async function startWPNow( return instance; }; - poolOptions.spawner = spawnAndSetup; + poolOptions.spawn = spawnAndSetup; const pool = new Pool(poolOptions); @@ -201,7 +216,7 @@ export default async function startWPNow( return instance; }; - poolOptions.spawner = spawnSetupAndLogin; + poolOptions.spawn = spawnSetupAndLogin; if ( isFirstTimeProject && From 65c28e2fa97a639810fa554d3705da7d4141bd13 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:33:19 -0400 Subject: [PATCH 38/56] Correcting default fatal handler --- packages/wp-now/src/pool.ts | 24 ++++++++++++------------ packages/wp-now/src/start-server.ts | 3 +-- packages/wp-now/src/wp-now.ts | 19 +++++++++++-------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index da0a5760..70885903 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -3,11 +3,11 @@ type instance = any; type request = (instance: instance) => Promise; export type poolOptions = { - spawn: () => Promise, - reap: (instance:instance) => void, - fatal: (instance:instance, error:any) => void, - maxRequests: number - maxJobs: number + spawn: () => Promise; + reap: (instance: instance) => void; + fatal: (instance: instance, error: any) => void; + maxRequests: number; + maxJobs: number; }; let childCount = 0; @@ -130,7 +130,7 @@ export class Pool { spawn: () => Promise; // Async callback to create new instances. fatal: (instance: instance, error: any) => any; // Async callback called on instance fatal errors. - reap: (instance: instance) => void; // Async callback called on destroyed instances. + reap: (instance: instance) => void; // Async callback called on destroyed instances. maxRequests: number; // Max requests to feed each instance maxJobs: number; // Max number of instances to maintain at once. @@ -146,15 +146,15 @@ export class Pool { maxRequests = 128, maxJobs = 1, spawn = async (): Promise => {}, - fatal = (instance: instance, request: request) => {}, + fatal = (instance: instance, error: any) => error, reap = (instance: instance) => {}, } = {}) { Object.defineProperties(this, { - maxRequests: {value: maxRequests}, - maxJobs: {value: maxJobs}, - spawn: {value: spawn}, - fatal: {value: fatal}, - reap: {value: reap}, + maxRequests: { value: maxRequests }, + maxJobs: { value: maxJobs }, + spawn: { value: spawn }, + fatal: { value: fatal }, + reap: { value: reap }, }); } diff --git a/packages/wp-now/src/start-server.ts b/packages/wp-now/src/start-server.ts index 04ada0ca..ce64c890 100644 --- a/packages/wp-now/src/start-server.ts +++ b/packages/wp-now/src/start-server.ts @@ -110,8 +110,7 @@ export async function startServer( try { resp = await pool.enqueue((php) => php.request(data)); - } - catch (error) { + } catch (error) { resp = error; } diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 80b149a3..42af0809 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -113,20 +113,23 @@ export default async function startWPNow( const poolOptions: poolOptions = { spawn: spawnInstance, - reap: (instance:NodePHP) => { + reap: (instance: NodePHP) => { try { instance.exit(); } catch { void 0; } }, - fatal: (instance:NodePHP, error:any) => new PHPResponse( - 500, - {}, - new TextEncoder().encode( - `500 Internal Server Error:\n\n${String((error && error.stack) ? error.stack : error)}` - ) - ), + fatal: (instance: NodePHP, error: any) => + new PHPResponse( + 500, + {}, + new TextEncoder().encode( + `500 Internal Server Error:\n\n${String( + error && error.stack ? error.stack : error + )}` + ) + ), maxRequests: options.maxRequests ?? 128, maxJobs: options.maxJobs ?? 1, }; From a1b809e4bc06bc453ac77d25af1afa051d6ebf22 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:58:14 -0400 Subject: [PATCH 39/56] Abstracting node pools --- packages/wp-now/src/node-pool.ts | 44 ++++++++++++++++++++++++++++++++ packages/wp-now/src/pool.ts | 14 ++++++---- packages/wp-now/src/wp-now.ts | 25 +++--------------- 3 files changed, 57 insertions(+), 26 deletions(-) create mode 100644 packages/wp-now/src/node-pool.ts diff --git a/packages/wp-now/src/node-pool.ts b/packages/wp-now/src/node-pool.ts new file mode 100644 index 00000000..ecf6fdf2 --- /dev/null +++ b/packages/wp-now/src/node-pool.ts @@ -0,0 +1,44 @@ +import { PHPResponse } from '@php-wasm/universal'; +import { NodePHP } from '@php-wasm/node'; +import { Pool } from './pool'; + +export type nodePoolOptions = { + spawn?: () => Promise; + reap?: (instance: NodePHP) => void; + fatal?: (instance: NodePHP, error: any) => any; + maxRequests?: number; + maxJobs?: number; +}; + +const defaultSpawn = async () => await NodePHP.load('8.2'); + +const defaultFatal = (instance: NodePHP, error: any) => + new PHPResponse( + 500, + {}, + new TextEncoder().encode( + `500 Internal Server Error:\n\n${String( + error && error.stack ? error.stack : error + )}` + ) + ); + +const defaultReap = (instance: NodePHP) => { + try { + instance.exit(); + } catch { + void 0; + } +}; + +export class NodePool extends Pool { + constructor({ + maxRequests = 128, + maxJobs = 1, + spawn = defaultSpawn, + fatal = defaultFatal, + reap = defaultReap, + } = {}) { + super({ maxRequests, maxJobs, spawn, fatal, reap }); + } +} diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 70885903..7684f5a8 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -4,10 +4,10 @@ type request = (instance: instance) => Promise; export type poolOptions = { spawn: () => Promise; - reap: (instance: instance) => void; - fatal: (instance: instance, error: any) => void; - maxRequests: number; - maxJobs: number; + reap?: (instance: instance) => void; + fatal?: (instance: instance, error: any) => any; + maxRequests?: number; + maxJobs?: number; }; let childCount = 0; @@ -145,10 +145,14 @@ export class Pool { constructor({ maxRequests = 128, maxJobs = 1, - spawn = async (): Promise => {}, + spawn = undefined, fatal = (instance: instance, error: any) => error, reap = (instance: instance) => {}, } = {}) { + if (!spawn) { + throw new Error('Spawn method is required for pool.'); + } + Object.defineProperties(this, { maxRequests: { value: maxRequests }, maxJobs: { value: maxJobs }, diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 42af0809..749bf45b 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -33,7 +33,7 @@ import getWordpressVersionsPath from './get-wordpress-versions-path'; import getSqlitePath from './get-sqlite-path'; import { Pool, poolOptions } from './pool'; -import { PHPResponse } from '@php-wasm/universal'; +import { NodePool, nodePoolOptions } from './node-pool'; function seemsLikeAPHPFile(path) { return path.endsWith('.php') || path.includes('.php/'); @@ -111,25 +111,8 @@ export default async function startWPNow( output?.log(`mode: ${options.mode}`); output?.log(`php: ${options.phpVersion}`); - const poolOptions: poolOptions = { + const poolOptions: nodePoolOptions = { spawn: spawnInstance, - reap: (instance: NodePHP) => { - try { - instance.exit(); - } catch { - void 0; - } - }, - fatal: (instance: NodePHP, error: any) => - new PHPResponse( - 500, - {}, - new TextEncoder().encode( - `500 Internal Server Error:\n\n${String( - error && error.stack ? error.stack : error - )}` - ) - ), maxRequests: options.maxRequests ?? 128, maxJobs: options.maxJobs ?? 1, }; @@ -147,7 +130,7 @@ export default async function startWPNow( poolOptions.spawn = spawnAndSetup; - const pool = new Pool(poolOptions); + const pool = new NodePool(poolOptions); return { php, phpInstances, options, pool }; } @@ -228,7 +211,7 @@ export default async function startWPNow( await activatePluginOrTheme(php, options); } - const pool = new Pool(poolOptions); + const pool = new NodePool(poolOptions); return { php, phpInstances, options, pool }; } From 102adbc32e5e1320e30a7112f40f0bc1441972b9 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:03:05 -0400 Subject: [PATCH 40/56] Tweaks. --- packages/wp-now/src/wp-now.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 749bf45b..995431c3 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -32,7 +32,6 @@ import getWpNowPath from './get-wp-now-path'; import getWordpressVersionsPath from './get-wordpress-versions-path'; import getSqlitePath from './get-sqlite-path'; -import { Pool, poolOptions } from './pool'; import { NodePool, nodePoolOptions } from './node-pool'; function seemsLikeAPHPFile(path) { @@ -51,7 +50,7 @@ export default async function startWPNow( php: NodePHP; phpInstances: NodePHP[]; options: WPNowOptions; - pool: Pool; + pool: NodePool; }> { const { documentRoot } = options; From 954d33fab457336c71440c6c7a930fddbc31d89d Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:28:29 -0400 Subject: [PATCH 41/56] Tweaks --- packages/wp-now/src/pool.ts | 38 ++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 7684f5a8..e51d6419 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -69,9 +69,10 @@ const reap = (pool: Pool) => { * @private */ const fatal = (pool: Pool, instance: instance, error: Error) => { + console.error(error); - if (pool.instanceInfo.has(instance)) { + if (instance && pool.instanceInfo.has(instance)) { const info = pool.instanceInfo.get(instance); info.active = false; pool.instanceInfo.delete(instance); @@ -170,12 +171,18 @@ export class Pool { */ async enqueue(item: request): Promise { reap(this); - await spawn(this); - const idleInstance = getIdleInstance(this); + let idleInstance; + try { + await spawn(this); + idleInstance = getIdleInstance(this); + } catch (error) { + return Promise.reject(fatal(this, idleInstance, error)); + } + + // Defer the callback if we don't have an idle instance available. if (!idleInstance) { - // Defer the callback if we don't have an idle instance available. this.backlog.push(item); // Split a promise open so it can be resolved or @@ -194,10 +201,10 @@ export class Pool { // after the instance processes a request, and optionally // will also kick off the next request. const onCompleted = (instance) => async () => { + this.running.delete(instance); reap(this); - const newInstances = await spawn(this); // Break out here if the backlog is empty. if (!this.backlog.length) { @@ -208,15 +215,6 @@ export class Pool { // so we can re-use it... let nextInstance = instance; - // ... but, if we've just spanwed a fresh - // instance, use that one instead. - if (newInstances.size) { - for (const instance of newInstances) { - nextInstance = instance; - break; - } - } - const next = this.backlog.shift(); const info = this.instanceInfo.get(nextInstance); @@ -226,6 +224,17 @@ export class Pool { let request; try { + const newInstances = await spawn(this); + + // ... but, if we've just spanwed a fresh + // instance, use that one instead. + if (newInstances.size) { + for (const instance of newInstances) { + nextInstance = instance; + break; + } + } + request = next(nextInstance); } catch (error) { // Grab the reject handler from the notfier @@ -270,7 +279,6 @@ export class Pool { let request; // If we've got an instance available, run the provided callback. - try { request = item(idleInstance); } catch (error) { From 6bc105d10f7c6fb019c81f29d70fe4dc9c31a73e Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:32:04 -0400 Subject: [PATCH 42/56] Tweaks --- packages/wp-now/src/pool.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index e51d6419..7cc43356 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -69,7 +69,6 @@ const reap = (pool: Pool) => { * @private */ const fatal = (pool: Pool, instance: instance, error: Error) => { - console.error(error); if (instance && pool.instanceInfo.has(instance)) { @@ -201,7 +200,6 @@ export class Pool { // after the instance processes a request, and optionally // will also kick off the next request. const onCompleted = (instance) => async () => { - this.running.delete(instance); reap(this); From b3c6a085fc94a4030dfd942348d072e8be3851a2 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:32:57 -0400 Subject: [PATCH 43/56] Tweaks --- packages/wp-now/src/pool.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 7cc43356..9ec704ad 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -129,8 +129,8 @@ export class Pool { instanceInfo = new Map(); // php => PoolInfo spawn: () => Promise; // Async callback to create new instances. - fatal: (instance: instance, error: any) => any; // Async callback called on instance fatal errors. - reap: (instance: instance) => void; // Async callback called on destroyed instances. + fatal: (instance: instance, error: any) => any; // Callback called on instance fatal errors. + reap: (instance: instance) => void; // Callback called on destroyed instances. maxRequests: number; // Max requests to feed each instance maxJobs: number; // Max number of instances to maintain at once. From fabcafa269730824d9f2356257a2cfea4e7f9d51 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Mon, 4 Dec 2023 00:47:05 -0500 Subject: [PATCH 44/56] Bumping versions. --- package-lock.json | 74 +++++++++++++------- package.json | 10 +-- packages/interactive-code-block/project.json | 2 +- 3 files changed, 55 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4fd7a01..3c0adedf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,11 +26,11 @@ "@codemirror/state": "6.2.0", "@codemirror/theme-one-dark": "6.1.1", "@codemirror/view": "6.9.3", - "@php-wasm/node": "0.1.56", - "@php-wasm/progress": "0.1.56", - "@php-wasm/universal": "0.1.56", - "@php-wasm/web": "0.1.57", - "@wp-playground/blueprints": "0.1.56", + "@php-wasm/node": "0.3.1", + "@php-wasm/progress": "0.3.1", + "@php-wasm/universal": "0.3.1", + "@php-wasm/web": "0.3.1", + "@wp-playground/blueprints": "0.3.1", "@wp-playground/client": "0.2.0", "classnames": "^2.3.2", "comlink": "^4.4.1", @@ -9621,37 +9621,57 @@ } }, "node_modules/@php-wasm/node": { - "version": "0.1.56", - "resolved": "https://registry.npmjs.org/@php-wasm/node/-/node-0.1.56.tgz", - "integrity": "sha512-HcIPR8QnBgNsgys+msHktUICG+ENNn3Xt6kjP1OJKA0Yi/Y9bATEe8DJx3RiPyrVDJsb8WLKB+aWq9KVXl8x8w==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@php-wasm/node/-/node-0.3.1.tgz", + "integrity": "sha512-KoDXhFmaVcqzYxbnk4Q1tdOnsPkOPwysRFAVdzfcuBATfAloLPdcDbSJxrHJHfzJeN9/ky7oFpDj7bHZcCLbEw==", "dependencies": { - "@php-wasm/universal": "0.1.56", - "@php-wasm/util": "0.1.56", + "@php-wasm/universal": "0.3.1", + "@php-wasm/util": "0.3.1", "comlink": "^4.4.1", "express": "4.18.2", "ws": "8.13.0", "yargs": "17.7.2" + }, + "engines": { + "node": ">=16.15.1", + "npm": ">=8.11.0" } }, "node_modules/@php-wasm/progress": { - "version": "0.1.56", - "resolved": "https://registry.npmjs.org/@php-wasm/progress/-/progress-0.1.56.tgz", - "integrity": "sha512-AJG+maCSLX2797lwWOYgEyqy3AXvjilhcmfO22oU/U730BuZVzeLbQJ+v+F08uhGfVeIwe8sqKP9ksl/wWDlkA==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@php-wasm/progress/-/progress-0.3.1.tgz", + "integrity": "sha512-nWWNcRhWvXuE0pO6o027SYUSt2YG+rQwpCUXQVREK9XJEeZHi6Q2b0GHkJbgvmTAnIPl9qBj4m4r9BF8+4Z7nQ==", + "engines": { + "node": ">=16.15.1", + "npm": ">=8.11.0" + } }, "node_modules/@php-wasm/universal": { - "version": "0.1.56", - "resolved": "https://registry.npmjs.org/@php-wasm/universal/-/universal-0.1.56.tgz", - "integrity": "sha512-qQCpYPrGdfh8PpaIUPOemMtw7bXCEBAThiAokIW5nu+51NRLK2y10Jpk2ce+/iOC9e6SLtpA6Hr2U8GrnPAXjA==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@php-wasm/universal/-/universal-0.3.1.tgz", + "integrity": "sha512-WFGQkzSPrHFJLVA32a9se3LunyA/wyfsQX8KEX7Y3TIZvnLeTg4OGLFSzCIM52SdjXsARE+wBX5JCY86AmwMfg==", + "engines": { + "node": ">=16.15.1", + "npm": ">=8.11.0" + } }, "node_modules/@php-wasm/util": { - "version": "0.1.56", - "resolved": "https://registry.npmjs.org/@php-wasm/util/-/util-0.1.56.tgz", - "integrity": "sha512-3UW9+kh9wZkWbcxBFv/qhYF8bzE2oPGSUzr0oDnnUJGDzCJnjL5OFcfU7WJD9lKTJ85F5+Hfbg3m/mOQIaniZQ==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@php-wasm/util/-/util-0.3.1.tgz", + "integrity": "sha512-1Pw2UB0RjMXByZp1eAvWJCubnBYv6SZb14yg5pyLKAI1fOqcu/WmGcUhGBVQPDRxdHg1AEkZdFX1cO18fpya/Q==", + "engines": { + "node": ">=16.15.1", + "npm": ">=8.11.0" + } }, "node_modules/@php-wasm/web": { - "version": "0.1.57", - "resolved": "https://registry.npmjs.org/@php-wasm/web/-/web-0.1.57.tgz", - "integrity": "sha512-yQIjjteuDwle3HwKr8bETUr9QdsdSK757q2jAMCDqidkJJ5URd3vOQigntV2agciCOxzy/zLMteWzKPq/NJbwA==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@php-wasm/web/-/web-0.3.1.tgz", + "integrity": "sha512-ShKJf4xdXuErqnh2bhuCKtmxR8JhwODuszNhSLophKVeZ98lfKM0ddCmlaYZS8WPTY2Ad0EjTKO26xP5IExQQw==", + "engines": { + "node": ">=16.15.1", + "npm": ">=8.11.0" + } }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -14382,9 +14402,13 @@ "link": true }, "node_modules/@wp-playground/blueprints": { - "version": "0.1.56", - "resolved": "https://registry.npmjs.org/@wp-playground/blueprints/-/blueprints-0.1.56.tgz", - "integrity": "sha512-Yvll3D8oLbLgFQLQfDxkgucyLLGi99L9wl0z4+MDmFg4hEegZfP1e+VhjcXHIH3if+2XjMsWKLbUvwwHudzJkg==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@wp-playground/blueprints/-/blueprints-0.3.1.tgz", + "integrity": "sha512-SrfXsOgt8KbwYr/blrj66qvu7NnT7Q5k+Hk+nrMPdigC3JqoguIoeHJwx9t0peH68GXTyHOod1mwZhHYndCx2Q==", + "engines": { + "node": ">=16.15.1", + "npm": ">=8.11.0" + } }, "node_modules/@wp-playground/client": { "version": "0.2.0", diff --git a/package.json b/package.json index eb317efc..2d3256df 100644 --- a/package.json +++ b/package.json @@ -30,11 +30,11 @@ "@codemirror/state": "6.2.0", "@codemirror/theme-one-dark": "6.1.1", "@codemirror/view": "6.9.3", - "@php-wasm/node": "0.1.56", - "@php-wasm/progress": "0.1.56", - "@php-wasm/universal": "0.1.56", - "@php-wasm/web": "0.1.57", - "@wp-playground/blueprints": "0.1.56", + "@php-wasm/node": "0.3.1", + "@php-wasm/progress": "0.3.1", + "@php-wasm/universal": "0.3.1", + "@php-wasm/web": "0.3.1", + "@wp-playground/blueprints": "0.3.1", "@wp-playground/client": "0.2.0", "classnames": "^2.3.2", "comlink": "^4.4.1", diff --git a/packages/interactive-code-block/project.json b/packages/interactive-code-block/project.json index a3242e1a..d1c76c65 100644 --- a/packages/interactive-code-block/project.json +++ b/packages/interactive-code-block/project.json @@ -14,7 +14,7 @@ "cwd": "dist/packages", "commands": [ "cp ../../packages/interactive-code-block/README.md ./interactive-code-block/", - "rm interactive-code-block/assets/php_5*", + "rm -f interactive-code-block/assets/php_5*", "rm interactive-code-block/assets/php_7_0*", "rm interactive-code-block/assets/php_7_1*", "rm interactive-code-block/assets/php_7_2*", From 56484b50ee3be43ffbe9818dcbed56c71f6d804a Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Mon, 4 Dec 2023 04:19:22 -0500 Subject: [PATCH 45/56] Non-working copy operation --- .../public/blueprint.json | 52 +++++++++---------- packages/wp-now/README.md | 31 ++++++----- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/packages/interactive-code-block/public/blueprint.json b/packages/interactive-code-block/public/blueprint.json index 768395f2..511ac063 100644 --- a/packages/interactive-code-block/public/blueprint.json +++ b/packages/interactive-code-block/public/blueprint.json @@ -1,28 +1,28 @@ { - "landingPage": "/wp-admin/post.php?post=4&action=edit", - "steps": [ - { - "step": "login", - "username": "admin", - "password": "password" - }, - { - "step": "installTheme", - "themeZipFile": { - "resource": "wordpress.org/themes", - "slug": "adventurer" - } - }, - { - "step": "installPlugin", - "pluginZipFile": { - "resource": "wordpress.org/plugins", - "slug": "interactive-code-block" - } - }, - { - "step": "runPHP", - "code": " 'Interactive code block demo!','post_content' => '
PD9waHAKCmVjaG8gIlRoaXMgY29kZSBzbmlwcGV0IGlzIGludGVyYWN0aXZlISI7Cg==
', 'post_status' => 'publish', 'post_type' => 'post',]);" - } - ] + "landingPage": "/wp-admin/post.php?post=4&action=edit", + "steps": [ + { + "step": "login", + "username": "admin", + "password": "password" + }, + { + "step": "installTheme", + "themeZipFile": { + "resource": "wordpress.org/themes", + "slug": "adventurer" + } + }, + { + "step": "installPlugin", + "pluginZipFile": { + "resource": "wordpress.org/plugins", + "slug": "interactive-code-block" + } + }, + { + "step": "runPHP", + "code": " 'Interactive code block demo!','post_content' => '
PD9waHAKCmVjaG8gIlRoaXMgY29kZSBzbmlwcGV0IGlzIGludGVyYWN0aXZlISI7Cg==
', 'post_status' => 'publish', 'post_type' => 'post',]);" + } + ] } diff --git a/packages/wp-now/README.md b/packages/wp-now/README.md index 4bb48fb1..0f898493 100644 --- a/packages/wp-now/README.md +++ b/packages/wp-now/README.md @@ -6,24 +6,23 @@ It uses automatic mode detection to provide a fast setup process, regardless of ![Demo GIF of wp-now](https://github.com/WordPress/wordpress-playground/assets/36432/474ecb85-d789-4db9-b820-a19c25da79ad) - ## Table of Contents -- [Requirements](#requirements) -- [Usage](#usage) - - [Automatic Modes](#automatic-modes) - - [Arguments](#arguments) -- [Technical Details](#technical-details) -- [Using Blueprints](#using-blueprints) - - [Defining Custom URLs](#defining-custom-urls) - - [Defining Debugging Constants](#defining-debugging-constants) -- [Known Issues](#known-issues) -- [Comparisons](#comparisons) - - [Laravel Valet](#laravel-valet) - - [wp-env](#wp-env) -- [Contributing](#contributing) -- [Testing](#testing) -- [Publishing](#publishing) +- [Requirements](#requirements) +- [Usage](#usage) + - [Automatic Modes](#automatic-modes) + - [Arguments](#arguments) +- [Technical Details](#technical-details) +- [Using Blueprints](#using-blueprints) + - [Defining Custom URLs](#defining-custom-urls) + - [Defining Debugging Constants](#defining-debugging-constants) +- [Known Issues](#known-issues) +- [Comparisons](#comparisons) + - [Laravel Valet](#laravel-valet) + - [wp-env](#wp-env) +- [Contributing](#contributing) +- [Testing](#testing) +- [Publishing](#publishing) ## Requirements From c59bea8bb1ed3dbd51c2bc9007dfe717c519564d Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Wed, 6 Dec 2023 00:15:16 -0500 Subject: [PATCH 46/56] Revering extra change. --- .../public/blueprint.json | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/interactive-code-block/public/blueprint.json b/packages/interactive-code-block/public/blueprint.json index 511ac063..768395f2 100644 --- a/packages/interactive-code-block/public/blueprint.json +++ b/packages/interactive-code-block/public/blueprint.json @@ -1,28 +1,28 @@ { - "landingPage": "/wp-admin/post.php?post=4&action=edit", - "steps": [ - { - "step": "login", - "username": "admin", - "password": "password" - }, - { - "step": "installTheme", - "themeZipFile": { - "resource": "wordpress.org/themes", - "slug": "adventurer" - } - }, - { - "step": "installPlugin", - "pluginZipFile": { - "resource": "wordpress.org/plugins", - "slug": "interactive-code-block" - } - }, - { - "step": "runPHP", - "code": " 'Interactive code block demo!','post_content' => '
PD9waHAKCmVjaG8gIlRoaXMgY29kZSBzbmlwcGV0IGlzIGludGVyYWN0aXZlISI7Cg==
', 'post_status' => 'publish', 'post_type' => 'post',]);" - } - ] + "landingPage": "/wp-admin/post.php?post=4&action=edit", + "steps": [ + { + "step": "login", + "username": "admin", + "password": "password" + }, + { + "step": "installTheme", + "themeZipFile": { + "resource": "wordpress.org/themes", + "slug": "adventurer" + } + }, + { + "step": "installPlugin", + "pluginZipFile": { + "resource": "wordpress.org/plugins", + "slug": "interactive-code-block" + } + }, + { + "step": "runPHP", + "code": " 'Interactive code block demo!','post_content' => '
PD9waHAKCmVjaG8gIlRoaXMgY29kZSBzbmlwcGV0IGlzIGludGVyYWN0aXZlISI7Cg==
', 'post_status' => 'publish', 'post_type' => 'post',]);" + } + ] } From c133a1e80bdbf1eba5168bc55f66373910795e09 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 7 Dec 2023 17:56:51 -0500 Subject: [PATCH 47/56] PR comments. --- packages/interactive-code-block/project.json | 2 +- .../public/blueprint.json | 52 +++++++++---------- packages/vscode-extension/project.json | 2 +- packages/wp-now/src/node-pool.ts | 8 +-- packages/wp-now/src/pool.ts | 24 ++++----- packages/wp-now/src/wp-now.ts | 18 ++----- 6 files changed, 48 insertions(+), 58 deletions(-) diff --git a/packages/interactive-code-block/project.json b/packages/interactive-code-block/project.json index d1c76c65..a3242e1a 100644 --- a/packages/interactive-code-block/project.json +++ b/packages/interactive-code-block/project.json @@ -14,7 +14,7 @@ "cwd": "dist/packages", "commands": [ "cp ../../packages/interactive-code-block/README.md ./interactive-code-block/", - "rm -f interactive-code-block/assets/php_5*", + "rm interactive-code-block/assets/php_5*", "rm interactive-code-block/assets/php_7_0*", "rm interactive-code-block/assets/php_7_1*", "rm interactive-code-block/assets/php_7_2*", diff --git a/packages/interactive-code-block/public/blueprint.json b/packages/interactive-code-block/public/blueprint.json index 768395f2..511ac063 100644 --- a/packages/interactive-code-block/public/blueprint.json +++ b/packages/interactive-code-block/public/blueprint.json @@ -1,28 +1,28 @@ { - "landingPage": "/wp-admin/post.php?post=4&action=edit", - "steps": [ - { - "step": "login", - "username": "admin", - "password": "password" - }, - { - "step": "installTheme", - "themeZipFile": { - "resource": "wordpress.org/themes", - "slug": "adventurer" - } - }, - { - "step": "installPlugin", - "pluginZipFile": { - "resource": "wordpress.org/plugins", - "slug": "interactive-code-block" - } - }, - { - "step": "runPHP", - "code": " 'Interactive code block demo!','post_content' => '
PD9waHAKCmVjaG8gIlRoaXMgY29kZSBzbmlwcGV0IGlzIGludGVyYWN0aXZlISI7Cg==
', 'post_status' => 'publish', 'post_type' => 'post',]);" - } - ] + "landingPage": "/wp-admin/post.php?post=4&action=edit", + "steps": [ + { + "step": "login", + "username": "admin", + "password": "password" + }, + { + "step": "installTheme", + "themeZipFile": { + "resource": "wordpress.org/themes", + "slug": "adventurer" + } + }, + { + "step": "installPlugin", + "pluginZipFile": { + "resource": "wordpress.org/plugins", + "slug": "interactive-code-block" + } + }, + { + "step": "runPHP", + "code": " 'Interactive code block demo!','post_content' => '
PD9waHAKCmVjaG8gIlRoaXMgY29kZSBzbmlwcGV0IGlzIGludGVyYWN0aXZlISI7Cg==
', 'post_status' => 'publish', 'post_type' => 'post',]);" + } + ] } diff --git a/packages/vscode-extension/project.json b/packages/vscode-extension/project.json index 22d56cbb..87569ded 100644 --- a/packages/vscode-extension/project.json +++ b/packages/vscode-extension/project.json @@ -46,7 +46,7 @@ "cp ./LICENSE dist/packages/vscode-extension", "cp packages/vscode-extension/package.json dist/packages/vscode-extension", "cp packages/vscode-extension/README.md dist/packages/vscode-extension", - "find node_modules/@php-wasm/node/ -type f -name php_*.wasm | while read FILE; do cp $FILE dist/packages/vscode-extension; done" + "cp node_modules/@php-wasm/node/*.wasm dist/packages/vscode-extension" ], "parallel": false } diff --git a/packages/wp-now/src/node-pool.ts b/packages/wp-now/src/node-pool.ts index ecf6fdf2..1d341f04 100644 --- a/packages/wp-now/src/node-pool.ts +++ b/packages/wp-now/src/node-pool.ts @@ -1,8 +1,8 @@ -import { PHPResponse } from '@php-wasm/universal'; +import { LatestSupportedPHPVersion, PHPResponse } from '@php-wasm/universal'; import { NodePHP } from '@php-wasm/node'; import { Pool } from './pool'; -export type nodePoolOptions = { +export type NodePoolOptions = { spawn?: () => Promise; reap?: (instance: NodePHP) => void; fatal?: (instance: NodePHP, error: any) => any; @@ -10,7 +10,7 @@ export type nodePoolOptions = { maxJobs?: number; }; -const defaultSpawn = async () => await NodePHP.load('8.2'); +const defaultSpawn = async () => await NodePHP.load(LatestSupportedPHPVersion); const defaultFatal = (instance: NodePHP, error: any) => new PHPResponse( @@ -27,7 +27,7 @@ const defaultReap = (instance: NodePHP) => { try { instance.exit(); } catch { - void 0; + // Do nothing. } }; diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 9ec704ad..41e703bb 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -1,11 +1,11 @@ -type instance = any; +type PhpInstance = any; -type request = (instance: instance) => Promise; +type request = (instance: PhpInstance) => Promise; -export type poolOptions = { - spawn: () => Promise; - reap?: (instance: instance) => void; - fatal?: (instance: instance, error: any) => any; +export type PoolOptions = { + spawn: () => Promise; + reap?: (instance: PhpInstance) => void; + fatal?: (instance: PhpInstance, error: any) => any; maxRequests?: number; maxJobs?: number; }; @@ -68,7 +68,7 @@ const reap = (pool: Pool) => { * @param error the actual error that got us here * @private */ -const fatal = (pool: Pool, instance: instance, error: Error) => { +const fatal = (pool: Pool, instance: PhpInstance, error: Error) => { console.error(error); if (instance && pool.instanceInfo.has(instance)) { @@ -84,7 +84,7 @@ const fatal = (pool: Pool, instance: instance, error: Error) => { * Find the next available idle instance. * @private */ -const getIdleInstance = (pool) => { +const getIdleInstance = (pool: Pool) => { const sorted = [...pool.instanceInfo].sort( (a, b) => a[1].requests - b[1].requests ); @@ -129,8 +129,8 @@ export class Pool { instanceInfo = new Map(); // php => PoolInfo spawn: () => Promise; // Async callback to create new instances. - fatal: (instance: instance, error: any) => any; // Callback called on instance fatal errors. - reap: (instance: instance) => void; // Callback called on destroyed instances. + fatal: (instance: PhpInstance, error: any) => any; // Callback called on instance fatal errors. + reap: (instance: PhpInstance) => void; // Callback called on destroyed instances. maxRequests: number; // Max requests to feed each instance maxJobs: number; // Max number of instances to maintain at once. @@ -146,8 +146,8 @@ export class Pool { maxRequests = 128, maxJobs = 1, spawn = undefined, - fatal = (instance: instance, error: any) => error, - reap = (instance: instance) => {}, + fatal = (instance: PhpInstance, error: any) => error, + reap = (instance: PhpInstance) => {}, } = {}) { if (!spawn) { throw new Error('Spawn method is required for pool.'); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 995431c3..8cef5086 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -32,7 +32,7 @@ import getWpNowPath from './get-wp-now-path'; import getWordpressVersionsPath from './get-wordpress-versions-path'; import getSqlitePath from './get-sqlite-path'; -import { NodePool, nodePoolOptions } from './node-pool'; +import { NodePool, NodePoolOptions } from './node-pool'; function seemsLikeAPHPFile(path) { return path.endsWith('.php') || path.includes('.php/'); @@ -82,18 +82,8 @@ export default async function startWPNow( ); } - const spawnInstance = async () => { - const php = await NodePHP.load(options.phpVersion, nodePHPOptions); - - php.mkdirTree(documentRoot); - php.chdir(documentRoot); - php.writeFile( - `${documentRoot}/index.php`, - ` + await NodePHP.load(options.phpVersion, nodePHPOptions); const php = phpInstances[0]; @@ -110,7 +100,7 @@ export default async function startWPNow( output?.log(`mode: ${options.mode}`); output?.log(`php: ${options.phpVersion}`); - const poolOptions: nodePoolOptions = { + const poolOptions: NodePoolOptions = { spawn: spawnInstance, maxRequests: options.maxRequests ?? 128, maxJobs: options.maxJobs ?? 1, From a6570405dbaf26c155db6f4da8b21c82da983a76 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 7 Dec 2023 19:41:14 -0500 Subject: [PATCH 48/56] PR comments. --- packages/wp-now/src/pool.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 41e703bb..9a42bdc1 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -17,10 +17,14 @@ let childCount = 0; * @private */ class PoolInfo { - id = childCount++; // Unique ID for debugging purposes. + id: number; // Unique ID for debugging purposes. + started: number; // Time spawned. requests = 0; // Total requests processed. - started = Date.now(); // Time spawned. active = false; // Whether instance is considered active. + constructor() { + this.id = childCount++; + this.started = Date.now(); + } } /** From 75bd3eb5d3d07a3dc17cd2c8373cd32000c68035 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 7 Dec 2023 21:28:14 -0500 Subject: [PATCH 49/56] Separating debug flags into its own PR --- .../src/executors/built-script/executor.ts | 5 ---- .../src/executors/built-script/schema.d.ts | 5 ---- .../src/executors/built-script/schema.json | 25 ------------------- packages/wp-now/src/run-cli.ts | 24 ------------------ 4 files changed, 59 deletions(-) diff --git a/packages/nx-extensions/src/executors/built-script/executor.ts b/packages/nx-extensions/src/executors/built-script/executor.ts index fb287845..4c93ad5e 100644 --- a/packages/nx-extensions/src/executors/built-script/executor.ts +++ b/packages/nx-extensions/src/executors/built-script/executor.ts @@ -7,11 +7,6 @@ const dirname = __dirname; export default async function runExecutor(options: BuiltScriptExecutorSchema) { const args = [ - ...(options['inspect'] ? ['--inspect-brk'] : []), - ...(options['inspect-brk'] ? ['--inspect-brk'] : []), - ...(options['trace-exit'] ? ['--trace-exit'] : []), - ...(options['trace-uncaught'] ? ['--trace-uncaught'] : []), - ...(options['trace-warnings'] ? ['--trace-warnings'] : []), '--loader', join(dirname, 'loader.mjs'), options.scriptPath, diff --git a/packages/nx-extensions/src/executors/built-script/schema.d.ts b/packages/nx-extensions/src/executors/built-script/schema.d.ts index 24aeacb2..f03329c1 100644 --- a/packages/nx-extensions/src/executors/built-script/schema.d.ts +++ b/packages/nx-extensions/src/executors/built-script/schema.d.ts @@ -1,9 +1,4 @@ export interface BuiltScriptExecutorSchema { scriptPath: string; - inspect: boolean; - 'inspect-brk': boolean; - 'trace-exit': boolean; - 'trace-uncaught': boolean; - 'trace-warnings': boolean; __unparsed__: string; } // eslint-disable-line diff --git a/packages/nx-extensions/src/executors/built-script/schema.json b/packages/nx-extensions/src/executors/built-script/schema.json index 58ed1a55..42ccfb44 100644 --- a/packages/nx-extensions/src/executors/built-script/schema.json +++ b/packages/nx-extensions/src/executors/built-script/schema.json @@ -10,31 +10,6 @@ "description": "Path of the script to run.", "x-prompt": "What script would you like to run?" }, - "inspect": { - "type": "boolean", - "description": "Use Node debugging client.", - "x-prompt": "Would you like to connect a Node debugging client?" - }, - "inspect-brk": { - "type": "boolean", - "description": "Use Node debugging client. Break immediately on script execution start.", - "x-prompt": "Would you like to connect a Node debugging client and break immediately on script execution start?" - }, - "trace-exit": { - "type": "boolean", - "description": "Prints a stack trace whenever an environment is exited proactively, i.e. invoking process.exit().", - "x-prompt": "Would you like print a stacktrace on exit?" - }, - "trace-uncaught": { - "type": "boolean", - "description": "Print stack traces for uncaught exceptions; usually, the stack trace associated with the creation of an Error is printed, whereas this makes Node.js also print the stack trace associated with throwing the value (which does not need to be an Error instance).", - "x-prompt": "Would you like print a stacktrace on uncaught promise rejection?" - }, - "trace-warnings": { - "type": "boolean", - "description": "Print stack traces for process warnings (including deprecations).", - "x-prompt": "Would you like print a stacktrace on warning?" - }, "__unparsed__": { "hidden": true, "type": "array", diff --git a/packages/wp-now/src/run-cli.ts b/packages/wp-now/src/run-cli.ts index 1397394b..796a98a2 100644 --- a/packages/wp-now/src/run-cli.ts +++ b/packages/wp-now/src/run-cli.ts @@ -85,30 +85,6 @@ export async function runCli() { describe: 'Max number of concurrent PHP instances.', type: 'number', }); - yargs.option('inspect', { - describe: 'Use Node debugging client.', - type: 'number', - }); - yargs.option('inspect-brk', { - describe: - 'Use Node debugging client. Break immediately on script execution start.', - type: 'number', - }); - yargs.option('trace-exit', { - describe: - 'Prints a stack trace whenever an environment is exited proactively, i.e. invoking process.exit().', - type: 'number', - }); - yargs.option('trace-uncaught', { - describe: - 'Print stack traces for uncaught exceptions; usually, the stack trace associated with the creation of an Error is printed, whereas this makes Node.js also print the stack trace associated with throwing the value (which does not need to be an Error instance).', - type: 'number', - }); - yargs.option('trace-warnings', { - describe: - 'Print stack traces for process warnings (including deprecations).', - type: 'number', - }); }, async (argv) => { const spinner = startSpinner('Starting the server...'); From c3b2269cc9d993b59ed974313282cba4426cfe52 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 21 Dec 2023 03:07:32 -0500 Subject: [PATCH 50/56] Incrementing verion numbers --- package-lock.json | 91 +++++++++++++++++++++++++------------ package.json | 12 ++--- packages/wp-now/src/pool.ts | 10 ++-- 3 files changed, 73 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c0adedf..1467b0a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,12 +26,12 @@ "@codemirror/state": "6.2.0", "@codemirror/theme-one-dark": "6.1.1", "@codemirror/view": "6.9.3", - "@php-wasm/node": "0.3.1", - "@php-wasm/progress": "0.3.1", - "@php-wasm/universal": "0.3.1", - "@php-wasm/web": "0.3.1", - "@wp-playground/blueprints": "0.3.1", - "@wp-playground/client": "0.2.0", + "@php-wasm/node": "0.5.2", + "@php-wasm/progress": "0.5.2", + "@php-wasm/universal": "0.5.2", + "@php-wasm/web": "0.5.2", + "@wp-playground/blueprints": "0.5.2", + "@wp-playground/client": "0.5.2", "classnames": "^2.3.2", "comlink": "^4.4.1", "compressible": "2.0.18", @@ -9621,12 +9621,13 @@ } }, "node_modules/@php-wasm/node": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@php-wasm/node/-/node-0.3.1.tgz", - "integrity": "sha512-KoDXhFmaVcqzYxbnk4Q1tdOnsPkOPwysRFAVdzfcuBATfAloLPdcDbSJxrHJHfzJeN9/ky7oFpDj7bHZcCLbEw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@php-wasm/node/-/node-0.5.2.tgz", + "integrity": "sha512-0tQqHtrBKrF02eBGJEfhcbdPd0L3sBvMGlnFQno7/szaYQy/9MANCeq5GeaUvwWUqg/0n4FhIE5yqZ7WTtqVUw==", "dependencies": { - "@php-wasm/universal": "0.3.1", - "@php-wasm/util": "0.3.1", + "@php-wasm/node-polyfills": "0.5.2", + "@php-wasm/universal": "0.5.2", + "@php-wasm/util": "0.5.2", "comlink": "^4.4.1", "express": "4.18.2", "ws": "8.13.0", @@ -9637,37 +9638,69 @@ "npm": ">=8.11.0" } }, + "node_modules/@php-wasm/node-polyfills": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@php-wasm/node-polyfills/-/node-polyfills-0.5.2.tgz", + "integrity": "sha512-AXrFGkjamy5dwSUC6lHKKAYZ3VFQKwhKJ9G+dDQgxg1nML6rP31M3X1pYFXPlkday3txnnsEGZ14NlVrSHxHWA==" + }, "node_modules/@php-wasm/progress": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@php-wasm/progress/-/progress-0.3.1.tgz", - "integrity": "sha512-nWWNcRhWvXuE0pO6o027SYUSt2YG+rQwpCUXQVREK9XJEeZHi6Q2b0GHkJbgvmTAnIPl9qBj4m4r9BF8+4Z7nQ==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@php-wasm/progress/-/progress-0.5.2.tgz", + "integrity": "sha512-3071UNLbVie0i0WHGwTBwnzeBjqy2n9A0aiuYAxvw5SHOPa2QJvW368u0zfBt958Ij0XQ4ZZHEoetqR2Y+bZog==", + "engines": { + "node": ">=16.15.1", + "npm": ">=8.11.0" + } + }, + "node_modules/@php-wasm/scopes": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@php-wasm/scopes/-/scopes-0.5.2.tgz", + "integrity": "sha512-bHyfx4xa6Khzuer6TTJIblbiprlxLd19uu3uAsmO0ZBW9Zv59ji6ITX0xKUWO/o8jLUKJ81FX6YphtoaDAM0KQ==", "engines": { "node": ">=16.15.1", "npm": ">=8.11.0" } }, "node_modules/@php-wasm/universal": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@php-wasm/universal/-/universal-0.3.1.tgz", - "integrity": "sha512-WFGQkzSPrHFJLVA32a9se3LunyA/wyfsQX8KEX7Y3TIZvnLeTg4OGLFSzCIM52SdjXsARE+wBX5JCY86AmwMfg==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@php-wasm/universal/-/universal-0.5.2.tgz", + "integrity": "sha512-jqkVYW9K6ZA3kTb45ogKNzVgRmNPojmoGy55jm78AjXY1XCdGar1yp+pp67nY6wMr0GWllWIc34AgDHzPacXoQ==", "engines": { "node": ">=16.15.1", "npm": ">=8.11.0" } }, "node_modules/@php-wasm/util": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@php-wasm/util/-/util-0.3.1.tgz", - "integrity": "sha512-1Pw2UB0RjMXByZp1eAvWJCubnBYv6SZb14yg5pyLKAI1fOqcu/WmGcUhGBVQPDRxdHg1AEkZdFX1cO18fpya/Q==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@php-wasm/util/-/util-0.5.2.tgz", + "integrity": "sha512-DSsB5jRCBQX7KL0BJxkDat/Ihlr5+k7sjkmJRJO3e+OsBAVZOm5bxo7sqg0A8Mxdqzfnve/JoUeCSIdL7w+rgg==", "engines": { "node": ">=16.15.1", "npm": ">=8.11.0" } }, "node_modules/@php-wasm/web": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@php-wasm/web/-/web-0.3.1.tgz", - "integrity": "sha512-ShKJf4xdXuErqnh2bhuCKtmxR8JhwODuszNhSLophKVeZ98lfKM0ddCmlaYZS8WPTY2Ad0EjTKO26xP5IExQQw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@php-wasm/web/-/web-0.5.2.tgz", + "integrity": "sha512-Qj6Q7EsPnABwz9eIggXrkmSUBaDbtqdI93diWLv+c3BskalvrXoIjY2f5W8MizuLdadIq28HQqsjkUhHy4xwMw==", + "dependencies": { + "@php-wasm/progress": "0.5.2", + "@php-wasm/universal": "0.5.2", + "@php-wasm/web-service-worker": "0.5.2", + "comlink": "^4.4.1" + }, + "engines": { + "node": ">=16.15.1", + "npm": ">=8.11.0" + } + }, + "node_modules/@php-wasm/web-service-worker": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@php-wasm/web-service-worker/-/web-service-worker-0.5.2.tgz", + "integrity": "sha512-EXVAT0i4ncH20mpCCUTdgthUEXd9IhijUqnSzrhYd4O9OiE7FbsKKtgVs6q76qD+0liePv2LpX4BbK3HtQ7GDQ==", + "dependencies": { + "@php-wasm/scopes": "0.5.2" + }, "engines": { "node": ">=16.15.1", "npm": ">=8.11.0" @@ -14402,18 +14435,18 @@ "link": true }, "node_modules/@wp-playground/blueprints": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@wp-playground/blueprints/-/blueprints-0.3.1.tgz", - "integrity": "sha512-SrfXsOgt8KbwYr/blrj66qvu7NnT7Q5k+Hk+nrMPdigC3JqoguIoeHJwx9t0peH68GXTyHOod1mwZhHYndCx2Q==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@wp-playground/blueprints/-/blueprints-0.5.2.tgz", + "integrity": "sha512-+XBKKl3/ivMMLkE/jDUKiuVpYesyRzEVY/OanBuFquUB86y22daTggRYBkp2SrKJwLBRaAt18xY66KyEptbyRw==", "engines": { "node": ">=16.15.1", "npm": ">=8.11.0" } }, "node_modules/@wp-playground/client": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@wp-playground/client/-/client-0.2.0.tgz", - "integrity": "sha512-BSXKJJZv4Yh5qs+gQuRJDMiyQc0uJPRNuRj1gdDhFz8p0NSE5fKfP/M9agnUlf3ijrpubCkt7hMpaLuynd2y3Q==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@wp-playground/client/-/client-0.5.2.tgz", + "integrity": "sha512-yONcME5TGKF9N47Gtlx+0zYGaXDZrMJcZUniRMV166zm0x6cOByXFDWUv7hz9gpI5o1g7SAlp14kPJ2hm0NRLQ==", "engines": { "node": ">=16.15.1", "npm": ">=8.11.0" diff --git a/package.json b/package.json index 2d3256df..44f0def1 100644 --- a/package.json +++ b/package.json @@ -30,12 +30,12 @@ "@codemirror/state": "6.2.0", "@codemirror/theme-one-dark": "6.1.1", "@codemirror/view": "6.9.3", - "@php-wasm/node": "0.3.1", - "@php-wasm/progress": "0.3.1", - "@php-wasm/universal": "0.3.1", - "@php-wasm/web": "0.3.1", - "@wp-playground/blueprints": "0.3.1", - "@wp-playground/client": "0.2.0", + "@php-wasm/node": "0.5.2", + "@php-wasm/progress": "0.5.2", + "@php-wasm/universal": "0.5.2", + "@php-wasm/web": "0.5.2", + "@wp-playground/blueprints": "0.5.2", + "@wp-playground/client": "0.5.2", "classnames": "^2.3.2", "comlink": "^4.4.1", "compressible": "2.0.18", diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 9a42bdc1..b7ab6aaf 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -158,11 +158,11 @@ export class Pool { } Object.defineProperties(this, { - maxRequests: { value: maxRequests }, - maxJobs: { value: maxJobs }, - spawn: { value: spawn }, - fatal: { value: fatal }, - reap: { value: reap }, + maxRequests: { value: maxRequests, writable: false, configurable: false, enumerable: false }, + maxJobs: { value: maxJobs, writable: false, configurable: false, enumerable: false }, + spawn: { value: spawn, writable: false, configurable: false, enumerable: false }, + fatal: { value: fatal, writable: false, configurable: false, enumerable: false }, + reap: { value: reap, writable: false, configurable: false, enumerable: false }, }); } From 1f9f6965db177c0298630fcbd61777a87534c7f2 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 21 Dec 2023 03:11:08 -0500 Subject: [PATCH 51/56] Formatting. --- packages/wp-now/src/pool.ts | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index b7ab6aaf..21994731 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -158,11 +158,36 @@ export class Pool { } Object.defineProperties(this, { - maxRequests: { value: maxRequests, writable: false, configurable: false, enumerable: false }, - maxJobs: { value: maxJobs, writable: false, configurable: false, enumerable: false }, - spawn: { value: spawn, writable: false, configurable: false, enumerable: false }, - fatal: { value: fatal, writable: false, configurable: false, enumerable: false }, - reap: { value: reap, writable: false, configurable: false, enumerable: false }, + maxRequests: { + value: maxRequests, + writable: false, + configurable: false, + enumerable: false, + }, + maxJobs: { + value: maxJobs, + writable: false, + configurable: false, + enumerable: false, + }, + spawn: { + value: spawn, + writable: false, + configurable: false, + enumerable: false, + }, + fatal: { + value: fatal, + writable: false, + configurable: false, + enumerable: false, + }, + reap: { + value: reap, + writable: false, + configurable: false, + enumerable: false, + }, }); } From f352bb84c1c67a45bbb27434c19b463f7a33b908 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Thu, 21 Dec 2023 03:17:24 -0500 Subject: [PATCH 52/56] Lint & Tests. --- packages/wp-now/src/wp-now.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 8cef5086..15406868 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -58,19 +58,6 @@ export default async function startWPNow( requestHandler: { documentRoot, absoluteUrl: options.absoluteUrl, - isStaticFilePath: (path) => { - try { - const fullPath = options.documentRoot + path; - return ( - php.fileExists(fullPath) && - !php.isDir(fullPath) && - !seemsLikeAPHPFile(fullPath) - ); - } catch (e) { - output?.error(e); - return false; - } - }, }, }; From 0ad0b9d636d7bbc303835fc5acb4d4d1463dcf1d Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Wed, 27 Dec 2023 05:28:56 -0500 Subject: [PATCH 53/56] PR comment tweaks. --- packages/wp-now/src/pool.ts | 1 - packages/wp-now/src/wp-now.ts | 4 ---- 2 files changed, 5 deletions(-) diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index 21994731..f7ed633e 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -18,7 +18,6 @@ let childCount = 0; */ class PoolInfo { id: number; // Unique ID for debugging purposes. - started: number; // Time spawned. requests = 0; // Total requests processed. active = false; // Whether instance is considered active. constructor() { diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 15406868..905a90a1 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -34,10 +34,6 @@ import getSqlitePath from './get-sqlite-path'; import { NodePool, NodePoolOptions } from './node-pool'; -function seemsLikeAPHPFile(path) { - return path.endsWith('.php') || path.includes('.php/'); -} - async function applyToInstances(phpInstances: NodePHP[], callback: Function) { for (let i = 0; i < phpInstances.length; i++) { await callback(phpInstances[i]); From 5d18b8165bfbcc70ee39c87279738c9b15f3e468 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Wed, 27 Dec 2023 05:38:16 -0500 Subject: [PATCH 54/56] Restoring codemirror dependency --- package-lock.json | 80 +++++++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 20866886..47c9c452 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@php-wasm/progress": "0.5.2", "@php-wasm/universal": "0.5.2", "@php-wasm/web": "0.5.2", + "@uiw/react-codemirror": "^4.21.20", "@wp-playground/blueprints": "0.5.2", "@wp-playground/client": "0.5.2", "classnames": "^2.3.2", @@ -2314,7 +2315,6 @@ "version": "7.21.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.13.11" }, @@ -2515,6 +2515,16 @@ "crelt": "^1.0.5" } }, + "node_modules/@codemirror/search": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.5.tgz", + "integrity": "sha512-PIEN3Ke1buPod2EHbJsoQwlbpkz30qGZKcnmH1eihq9+bPQx8gelauUwLYaY4vBOuBAuEhmpDLii4rj/uO0yMA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, "node_modules/@codemirror/state": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz", @@ -13297,6 +13307,57 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.21.21", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.21.21.tgz", + "integrity": "sha512-+0i9dPrRSa8Mf0CvyrMvnAhajnqwsP3IMRRlaHDRgsSGL8igc4z7MhvUPn+7cWFAAqWzQRhMdMSWzo6/TEa3EA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.21.21", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.21.21.tgz", + "integrity": "sha512-PaxBMarufMWoR0qc5zuvBSt76rJ9POm9qoOaJbqRmnNL2viaF+d+Paf2blPSlm1JSnqn7hlRjio+40nZJ9TKzw==", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.21.21", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@use-gesture/core": { "version": "10.2.27", "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.2.27.tgz", @@ -20896,6 +20957,20 @@ "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", "dev": true }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/collapse-white-space": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", @@ -40125,8 +40200,7 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regenerator-transform": { "version": "0.15.1", diff --git a/package.json b/package.json index 119d2244..829ab29c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@php-wasm/progress": "0.5.2", "@php-wasm/universal": "0.5.2", "@php-wasm/web": "0.5.2", + "@uiw/react-codemirror": "^4.21.20", "@wp-playground/blueprints": "0.5.2", "@wp-playground/client": "0.5.2", "classnames": "^2.3.2", From b09b13bc8c5402250833f61996c7de797aa5d1d0 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Wed, 27 Dec 2023 05:39:20 -0500 Subject: [PATCH 55/56] Revering extra change --- packages/wp-now/README.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/wp-now/README.md b/packages/wp-now/README.md index 0f898493..4bb48fb1 100644 --- a/packages/wp-now/README.md +++ b/packages/wp-now/README.md @@ -6,23 +6,24 @@ It uses automatic mode detection to provide a fast setup process, regardless of ![Demo GIF of wp-now](https://github.com/WordPress/wordpress-playground/assets/36432/474ecb85-d789-4db9-b820-a19c25da79ad) + ## Table of Contents -- [Requirements](#requirements) -- [Usage](#usage) - - [Automatic Modes](#automatic-modes) - - [Arguments](#arguments) -- [Technical Details](#technical-details) -- [Using Blueprints](#using-blueprints) - - [Defining Custom URLs](#defining-custom-urls) - - [Defining Debugging Constants](#defining-debugging-constants) -- [Known Issues](#known-issues) -- [Comparisons](#comparisons) - - [Laravel Valet](#laravel-valet) - - [wp-env](#wp-env) -- [Contributing](#contributing) -- [Testing](#testing) -- [Publishing](#publishing) +- [Requirements](#requirements) +- [Usage](#usage) + - [Automatic Modes](#automatic-modes) + - [Arguments](#arguments) +- [Technical Details](#technical-details) +- [Using Blueprints](#using-blueprints) + - [Defining Custom URLs](#defining-custom-urls) + - [Defining Debugging Constants](#defining-debugging-constants) +- [Known Issues](#known-issues) +- [Comparisons](#comparisons) + - [Laravel Valet](#laravel-valet) + - [wp-env](#wp-env) +- [Contributing](#contributing) +- [Testing](#testing) +- [Publishing](#publishing) ## Requirements From 0bd122211d4a5e2ed672cb1ccc366da3ba1d8803 Mon Sep 17 00:00:00 2001 From: Sean Morris <640101+seanmorris@users.noreply.github.com> Date: Wed, 27 Dec 2023 05:42:44 -0500 Subject: [PATCH 56/56] PR comment tweaks. --- packages/wp-now/README.md | 31 +++++++++++++++---------------- packages/wp-now/src/pool.ts | 1 - 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/packages/wp-now/README.md b/packages/wp-now/README.md index 4bb48fb1..0f898493 100644 --- a/packages/wp-now/README.md +++ b/packages/wp-now/README.md @@ -6,24 +6,23 @@ It uses automatic mode detection to provide a fast setup process, regardless of ![Demo GIF of wp-now](https://github.com/WordPress/wordpress-playground/assets/36432/474ecb85-d789-4db9-b820-a19c25da79ad) - ## Table of Contents -- [Requirements](#requirements) -- [Usage](#usage) - - [Automatic Modes](#automatic-modes) - - [Arguments](#arguments) -- [Technical Details](#technical-details) -- [Using Blueprints](#using-blueprints) - - [Defining Custom URLs](#defining-custom-urls) - - [Defining Debugging Constants](#defining-debugging-constants) -- [Known Issues](#known-issues) -- [Comparisons](#comparisons) - - [Laravel Valet](#laravel-valet) - - [wp-env](#wp-env) -- [Contributing](#contributing) -- [Testing](#testing) -- [Publishing](#publishing) +- [Requirements](#requirements) +- [Usage](#usage) + - [Automatic Modes](#automatic-modes) + - [Arguments](#arguments) +- [Technical Details](#technical-details) +- [Using Blueprints](#using-blueprints) + - [Defining Custom URLs](#defining-custom-urls) + - [Defining Debugging Constants](#defining-debugging-constants) +- [Known Issues](#known-issues) +- [Comparisons](#comparisons) + - [Laravel Valet](#laravel-valet) + - [wp-env](#wp-env) +- [Contributing](#contributing) +- [Testing](#testing) +- [Publishing](#publishing) ## Requirements diff --git a/packages/wp-now/src/pool.ts b/packages/wp-now/src/pool.ts index f7ed633e..f1e55a1a 100644 --- a/packages/wp-now/src/pool.ts +++ b/packages/wp-now/src/pool.ts @@ -22,7 +22,6 @@ class PoolInfo { active = false; // Whether instance is considered active. constructor() { this.id = childCount++; - this.started = Date.now(); } }