Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Website query API: Continue plugin installs on error #605

Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
797dce0
Implement callback onBlueprintStepError to allow plugin installs to c…
eliot-akira Jul 19, 2023
71f32d9
Format
eliot-akira Jul 19, 2023
c6fbe1f
Export type OnStepError
eliot-akira Jul 19, 2023
38791f2
Format
eliot-akira Jul 19, 2023
71b2467
Catch resource fetch errors to let others continue to resolve
eliot-akira Jul 25, 2023
486a632
Website: Until we have a way to show notifications, output console er…
eliot-akira Jul 25, 2023
bc803cd
Define default onStepError callback that rethrows
eliot-akira Jul 25, 2023
5fb2d41
Add comment to clarify how a blueprint step's resources are resolved
eliot-akira Aug 5, 2023
1281355
Implement onRunningSteps for success/error handling per step; onSteps…
eliot-akira Aug 8, 2023
4681f81
Update types
eliot-akira Aug 8, 2023
4722601
Format
eliot-akira Aug 8, 2023
69eb4af
Website: Implement onRunningBlueprintSteps handler that allows plugin…
eliot-akira Aug 8, 2023
978a3fa
Merge from upstream/trunk and resolve conflict
eliot-akira Dec 11, 2023
c8467b1
Refactor as onStepProgress
eliot-akira Dec 11, 2023
06d7f26
Add blueprint step option `failMode` with value `abort` or `skip`, an…
eliot-akira Feb 8, 2024
df14575
Refactor `OnStepProgress` and `OnStepsCompleted` into `onStepError` a…
eliot-akira Feb 8, 2024
4243e59
For fail mode "skip", resolve step status instead of rejecting it on …
eliot-akira Feb 8, 2024
b32c27b
compileBlueprint option shouldBoot returns boolean
eliot-akira Feb 8, 2024
c7b7920
Use shared type StepFailMode
eliot-akira Feb 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/playground/blueprints/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ export type {
CompiledBlueprint,
CompileBlueprintOptions,
OnStepCompleted,
OnStepsCompleted,
OnRunningSteps,
PromiseMap,
PromiseExecutor,
CompletedStatus,
} from './lib/compile';
export type {
CachedResource,
Expand Down
101 changes: 87 additions & 14 deletions packages/playground/blueprints/src/lib/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,36 @@ export interface CompiledBlueprint {

export type OnStepCompleted = (output: any, step: StepDefinition) => any;

export type CompletedStatus = Map<StepDefinition, boolean>;
export type OnStepsCompleted = (completedStatus: CompletedStatus) => void;
export type OnRunningSteps = (progressMap: PromiseMap) => void;

export type PromiseMap = Map<StepDefinition, Promise<void>>;
export type PromiseExecutor = {
resolve: (result: any) => void;
reject: (error: any) => void;
};
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved

export interface CompileBlueprintOptions {
/** Optional progress tracker to monitor progress */
progress?: ProgressTracker;
/** Optional semaphore to control access to a shared resource */
semaphore?: Semaphore;
/** Optional callback with step output */
onStepCompleted?: OnStepCompleted;
onRunningSteps?: OnRunningSteps;
onStepsCompleted?: OnStepsCompleted;
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Convert a Promise into a boolean indicating if it resolved or rejeted.
*/
const didResolve = <P extends Promise<any>>(promise: P): Promise<boolean> =>
Copy link
Collaborator

@adamziel adamziel Jan 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didResolve() === false suggests the promise may still be pending. I wonder what would be a more descriptive name here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was tough to find a suitable naming for this didResolve function. I consulted MDN: Promise to see what the standard terminology is.

A Promise is in one of these states:

  • pending: initial state, neither fulfilled nor rejected.
  • fulfilled: meaning that the operation was completed successfully.
  • rejected: meaning that the operation failed.

A promise is said to be settled if it is either fulfilled or rejected, but not pending.

In the end I settled on a verbose name isSettledPromiseResolved, to make it clear that it's checking an already settled promise for its resolved status.

promise.then(
() => true,
() => false
);

/**
* Compiles Blueprint into a form that can be executed.
*
Expand All @@ -71,6 +92,14 @@ export function compileBlueprint(
progress = new ProgressTracker(),
semaphore = new Semaphore({ concurrency: 3 }),
onStepCompleted = () => {},
onRunningSteps = (progressMap: PromiseMap) => {
for (const promise of progressMap.values()) {
promise.catch((error) => {
throw error;
});
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
}
},
onStepsCompleted,
}: CompileBlueprintOptions = {}
): CompiledBlueprint {
blueprint = {
Expand Down Expand Up @@ -116,33 +145,77 @@ export function compileBlueprint(
),
},
run: async (playground: UniversalPHP) => {
const progressMap = new Map<StepDefinition, Promise<void>>();
const progressPromises = new Map<StepDefinition, PromiseExecutor>();
for (const { step } of compiled) {
progressMap.set(
step,
new Promise((resolve, reject) => {
progressPromises.set(step, { resolve, reject });
})
);
}
onRunningSteps(progressMap);

try {
// Start resolving resources early
for (const { resources } of compiled) {
for (const { resources, step } of compiled) {
const stepPromise = progressPromises.get(
step
) as PromiseExecutor;
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
for (const resource of resources) {
resource.setPlayground(playground);
if (resource.isAsync) {
resource.resolve();
resource.resolve().catch(function (error) {
dmsnell marked this conversation as resolved.
Show resolved Hide resolved
stepPromise.reject(error);
});
}
}
}

/**
* When a compiled step's `run` method is called, it
* uses `resolveArguments` to await dependent resources.
*/
for (const { run, step } of compiled) {
const result = await run(playground);
onStepCompleted(result, step);
const stepPromise = progressPromises.get(
step
) as PromiseExecutor;
try {
const result = await run(playground);
stepPromise.resolve(result);
onStepCompleted(result, step);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the onStepCompleted handler throws an error, the stepPromise?.reject(error); will run. It would be nice to just rethrow that error so the API consumer may notice and correct it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of rethrowing, the two lines stepStatus?.resolve(result) and onStepCompleted have been moved below the try/catch clause. If the latter throws (by the user or an unexpected error), it bubbles up to the API consumer, the caller of CompiledBlueprint.run().

} catch (error) {
stepPromise.reject(error);
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
}
}
} finally {
try {
await (playground as any).goTo(
blueprint.landingPage || '/'
const completedStatus = new Map();
for (const { step } of compiled) {
completedStatus.set(
step,
await didResolve(progressMap.get(step) as Promise<any>)
);
} catch (e) {
/*
* NodePHP exposes no goTo method.
* We can't use `goto` in playground here,
* because it may be a Comlink proxy object
* with no such method.
*/
}

// Either someone made a choice to boot or all steps succeeded.
const shouldBoot = onStepsCompleted
? await onStepsCompleted(completedStatus)
: await didResolve(Promise.all([...progressMap.values()]));

if (shouldBoot) {
try {
await (playground as any)?.goTo(
blueprint.landingPage || '/'
);
} catch (e) {
/*
* NodePHP exposes no goTo method.
* We can't use `goto` in playground here,
* because it may be a Comlink proxy object
* with no such method.
*/
}
}
progress.finish();
}
Expand Down
6 changes: 6 additions & 0 deletions packages/playground/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
Blueprint,
compileBlueprint,
OnStepCompleted,
OnStepsCompleted,
OnRunningSteps,
runBlueprintSteps,
} from '@wp-playground/blueprints';
import { consumeAPI } from '@php-wasm/web';
Expand All @@ -46,6 +48,8 @@ export interface StartPlaygroundOptions {
disableProgressBar?: boolean;
blueprint?: Blueprint;
onBlueprintStepCompleted?: OnStepCompleted;
onBlueprintStepsCompleted?: OnStepsCompleted;
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
onRunningBlueprintSteps?: OnRunningSteps;
}

/**
Expand All @@ -62,6 +66,7 @@ export async function startPlaygroundWeb({
progressTracker = new ProgressTracker(),
disableProgressBar,
onBlueprintStepCompleted,
onRunningBlueprintSteps,
}: StartPlaygroundOptions): Promise<PlaygroundClient> {
assertValidRemote(remoteUrl);
allowStorageAccessByUserActivation(iframe);
Expand All @@ -76,6 +81,7 @@ export async function startPlaygroundWeb({
const compiled = compileBlueprint(blueprint, {
progress: progressTracker.stage(0.5),
onStepCompleted: onBlueprintStepCompleted,
onRunningSteps: onRunningBlueprintSteps,
});
const playground = await doStartPlaygroundWeb(
iframe,
Expand Down
14 changes: 14 additions & 0 deletions packages/playground/website/src/lib/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { Blueprint, startPlaygroundWeb } from '@wp-playground/client';
import type { PlaygroundClient } from '@wp-playground/client';
import type { PromiseMap } from '@wp-playground/blueprints';
import { buildVersion } from './config';

interface UsePlaygroundOptions {
Expand Down Expand Up @@ -36,10 +37,23 @@ export function usePlayground({ blueprint, storage }: UsePlaygroundOptions) {
remoteUrl.searchParams.set('storage', storage);
}

const onRunningBlueprintSteps = (progressMap: PromiseMap) => {
for (const [step, promise] of progressMap.entries()) {
eliot-akira marked this conversation as resolved.
Show resolved Hide resolved
promise.catch((error: any) => {
if (step.step === 'installPlugin') {
alert(`Failed to install: ${step.pluginZipFile}`);
} else {
throw error;
}
});
}
};

startPlaygroundWeb({
iframe,
remoteUrl: remoteUrl.toString(),
blueprint,
onRunningBlueprintSteps,
}).then(async (playground) => {
playground.onNavigation((url) => setUrl(url));
setPlayground(() => playground);
Expand Down