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

feat(worker-playground): delay preview environment setup #6962

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
2 changes: 2 additions & 0 deletions packages/quick-edit/functions/_middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const onRequest = async ({
"package.json": true,
"wrangler.toml": true,
},
"files.autoSave": "afterDelay",
"files.autoSaveDelay": 200,
"telemetry.telemetryLevel": "off",
"window.menuBarVisibility": "hidden",
},
Expand Down
1 change: 0 additions & 1 deletion packages/workers-playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"@cloudflare/style-container": "7.12.2",
"@cloudflare/style-provider": "^3.1.0",
"@cloudflare/util-en-garde": "^8.0.10",
"@cloudflare/util-hooks": "^1.2.0",
"@cloudflare/workers-editor-shared": "workspace:*",
"@cloudflare/workers-tsconfig": "workspace:*",
"glob-to-regexp": "^0.4.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function EditorPane() {
: undefined
}
onChange={({ entrypoint, files }) =>
draftWorker.preview({ entrypoint, modules: files })
draftWorker.updateDraft({ entrypoint, modules: files })
}
/>
</FrameErrorBoundary>
Expand Down
55 changes: 35 additions & 20 deletions packages/workers-playground/src/QuickEditor/HTTPTab/HTTPTab.tsx
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did this file need changed? It seems from a quick skim that it's doing roughly the same thing

Copy link
Member Author

Choose a reason for hiding this comment

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

We didn't share the URLBar implementation between the two tabs unlike the dashboard. Do you want me to fix that?

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Toast } from "@cloudflare/component-toast";
import { Div, Form, Label, Output } from "@cloudflare/elements";
import { isDarkMode, theme } from "@cloudflare/style-const";
import { createStyledComponent } from "@cloudflare/style-container";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { FrameError } from "../FrameErrorBoundary";
import { InputField } from "../InputField";
import { ServiceContext } from "../QuickEditor";
Expand Down Expand Up @@ -57,22 +57,26 @@ export function HTTPTab() {
setPreviewUrl,
isPreviewUpdating,
previewError,
preview,
} = useContext(ServiceContext);
const [method, setMethod] = useState<HTTPMethod>("GET");
const [headers, setHeaders] = useState<HeaderEntry[]>([]);
const [body, setBody] = useState("");
const [response, setResponse] = useState<Response | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState(previewUrl);
const [refreshTimestamp, setRefreshTimestamp] = useState<string>();

const hasBody = method !== "HEAD" && method !== "GET" && method !== "OPTIONS";
useEffect(() => {
setUrl(previewUrl);
}, [previewUrl]);
Comment on lines +67 to +72
Copy link
Member Author

Choose a reason for hiding this comment

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

We need an additional url state to load if the url is changed on submit. This is useful to decide whether we need to re-fetch the response from the same url.


const onSendRequest = useCallback(
async (e?: React.FormEvent<HTMLFormElement>) => {
e?.preventDefault();
const hasBody = method !== "HEAD" && method !== "GET" && method !== "OPTIONS";

useEffect(() => {
async function sendRequest() {
if (previewHash !== undefined && previewUrl !== undefined) {
try {
setPreviewUrl(previewUrl);
setIsLoading(true);
setResponse(
await fetchWorker(
Expand All @@ -89,22 +93,33 @@ export function HTTPTab() {
setIsLoading(false);
}
}
},
[previewHash, setPreviewUrl, previewUrl, method, headers, hasBody, body]
);
const ensureDevtoolsConnected = useRef(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why has this been deleted?

Copy link
Member Author

Choose a reason for hiding this comment

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

The variable ensureDevtoolsConnected was used to send the initial request. This is no longer needed as we have the useEffect to keep track of all the config and send a new request when needed.

useEffect(() => {
if (!ensureDevtoolsConnected.current && previewHash && !isLoading) {
void onSendRequest();
ensureDevtoolsConnected.current = true;
}
}, [previewHash, isLoading, onSendRequest]);

void sendRequest();
}, [
refreshTimestamp,
previewHash,
previewUrl,
method,
headers,
hasBody,
body,
]);

return (
<Div display="flex" flexDirection="column" width="100%">
<Form
display="flex"
onSubmit={(e) => void onSendRequest(e)}
onSubmit={(e) => {
e.preventDefault();
preview();

if (url === previewUrl) {
setRefreshTimestamp(new Date().toISOString());
Copy link
Member Author

Choose a reason for hiding this comment

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

The timestamp has no use but trigger the effect above (sendRequest) to re-run.

} else {
setPreviewUrl(url);
}
}}
p={2}
gap={2}
borderBottom="1px solid"
Expand All @@ -124,10 +139,10 @@ export function HTTPTab() {
/>
<InputField
name="http_request_url"
value={previewUrl}
value={url}
autoComplete="off"
spellCheck={false}
onChange={(e) => setPreviewUrl(e.target.value)}
onChange={(e) => setUrl(e.target.value)}
mb={0}
/>
<Button
Expand All @@ -138,8 +153,8 @@ export function HTTPTab() {
disabled={
!previewHash ||
Boolean(previewError) ||
!previewUrl ||
!previewUrl.startsWith("/")
!url ||
!url.startsWith("/")
}
data-tracking-name="send http tab request"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@ function PreviewTabImplementation() {
return (
<Div display="flex" flexDirection="column" width="100%">
<UrlBar
initialURL={draftWorker.previewUrl}
onSubmit={(url) => {
draftWorker.preview();

if (url === draftWorker?.previewUrl) {
refresh();
} else {
draftWorker.setPreviewUrl(url);
}
}}
loading={isLoading}
loading={isLoading || draftWorker.isPreviewUpdating}
/>
{!firstLoad && !draftWorker?.previewError && (
<Div
Expand Down
41 changes: 20 additions & 21 deletions packages/workers-playground/src/QuickEditor/PreviewTab/UrlBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Button } from "@cloudflare/component-button";
import { Div } from "@cloudflare/elements";
import { createComponent } from "@cloudflare/style-container";
import { useState } from "react";
import { useEffect, useState } from "react";
import { InputField } from "../InputField";
import type React from "react";

Expand All @@ -16,43 +16,42 @@ const StyledForm = createComponent(
);

type Props = {
initialURL: string;
onSubmit: (url: string) => void;
loading: boolean;
};

export default function URLBar(props: Props) {
const [value, setValue] = useState("/");
export default function URLBar({ initialURL, onSubmit, loading }: Props) {
const [url, setUrl] = useState(initialURL);

const onChangeInputValue = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value: newValue } = e.target;
if (!newValue.startsWith("/")) {
setValue(`/${newValue}`);
} else {
setValue(newValue);
}
};
useEffect(() => {
setUrl(initialURL);
}, [initialURL]);

return (
<StyledForm
onSubmit={(e: React.FormEvent) => {
e.preventDefault();
props.onSubmit(value);
onSubmit(url);
}}
>
<Div display="flex" gap={2} width="100%">
<InputField
name="url"
autoComplete="off"
value={value}
onChange={onChangeInputValue}
value={url}
onChange={(event) => {
let newURL = event.target.value;

if (!newURL.startsWith("/")) {
newURL = `/${newURL}`;
}

setUrl(newURL);
}}
/>
<Button
type="primary"
inverted={true}
submit={true}
loading={props.loading}
>
Send
<Button type="primary" inverted={true} submit={true} loading={loading}>
Go
</Button>
</Div>
</StyledForm>
Expand Down
19 changes: 12 additions & 7 deletions packages/workers-playground/src/QuickEditor/QuickEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@ export default function QuickEditor() {
window.location.hash.slice(1)
);

function updateWorkerHash(hash: string) {
history.replaceState(null, "", hash);
}
const draftWorker = useDraftWorker(initialWorkerContentHash);

const draftWorker = useDraftWorker(
initialWorkerContentHash,
updateWorkerHash
);
useEffect(() => {
function updateWorkerHash(hash: string) {
history.replaceState(null, "", hash);
}

const hash = draftWorker.previewHash?.serialised;

if (hash) {
updateWorkerHash(`/playground#${hash}`);
}
}, [draftWorker.previewHash?.serialised]);

useEffect(() => {
if (initialWorkerContentHash === "") {
Expand Down
73 changes: 28 additions & 45 deletions packages/workers-playground/src/QuickEditor/useDraftWorker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { eg } from "@cloudflare/util-en-garde";
import { useDebounce } from "@cloudflare/util-hooks";
import lzstring from "lz-string";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import useSWR from "swr";
import { v4 } from "uuid";
import { getPlaygroundWorker } from "./getPlaygroundWorker";
Expand Down Expand Up @@ -139,14 +138,9 @@ async function compressWorker(worker: FormData) {
);
}

async function updatePreviewHash(
content: Worker,
updateWorkerHash: (hash: string) => void
): Promise<PreviewHash> {
async function updatePreviewHash(content: Worker): Promise<PreviewHash> {
const worker = serialiseWorker(content);
const serialised = await compressWorker(worker);
const playgroundUrl = `/playground#${serialised}`;
updateWorkerHash(playgroundUrl);

const res = await fetch("/playground/api/worker", {
method: "POST",
Expand All @@ -170,16 +164,13 @@ async function updatePreviewHash(
};
}

const DEBOUNCE_TIMEOUT = 1000;

export function useDraftWorker(
initialHash: string,
updateWorkerHash: (hash: string) => void
): {
export function useDraftWorker(initialHash: string): {
isLoading: boolean;
service: Worker | null;
previewService: Worker | null;
devtoolsUrl: string | undefined;
preview: (content: Pick<Worker, "entrypoint" | "modules">) => void;
updateDraft: (content: Pick<Worker, "entrypoint" | "modules">) => void;
preview: () => void;
previewHash: PreviewHash | undefined;
previewError: string | undefined;
parseError: string | undefined;
Expand All @@ -191,30 +182,23 @@ export function useDraftWorker(
data: worker,
isLoading,
error,
} = useSWR(initialHash, getPlaygroundWorker);
} = useSWR(initialHash, getPlaygroundWorker, {
// There is no need to revalidate playground worker as it is rarely updated
revalidateOnFocus: false,
});

const [draftWorker, setDraftWorker] =
useState<Pick<Worker, "entrypoint" | "modules">>();

const [previewWorker, setPreviewWorker] = useState(draftWorker);
const [previewHash, setPreviewHash] = useState<PreviewHash>();
const [previewError, setPreviewError] = useState<string>();
const [devtoolsUrl, setDevtoolsUrl] = useState<string>();

const updatePreview = useDebounce(
async (wk?: Pick<Worker, "entrypoint" | "modules">) => {
setDraftWorker(wk);
if (worker === undefined) {
return;
}
useEffect(() => {
async function updatePreview(content: Worker) {
try {
setIsPreviewUpdating(true);
const hash = await updatePreviewHash(
{
...worker,
...(wk ?? draftWorker),
},
updateWorkerHash
);
const hash = await updatePreviewHash(content);
setPreviewHash(hash);
setDevtoolsUrl(hash.devtoolsUrl);
} catch (e: unknown) {
Expand All @@ -225,28 +209,27 @@ export function useDraftWorker(
} finally {
setIsPreviewUpdating(false);
}
},
DEBOUNCE_TIMEOUT
);
}

const initialPreview = useRef(false);
useEffect(() => {
if (worker && !initialPreview.current) {
initialPreview.current = true;
setIsPreviewUpdating(true);
void updatePreview(worker).then(() => setIsPreviewUpdating(false));
if (worker) {
void updatePreview({
...worker,
...previewWorker,
});
}
}, [updatePreview, worker]);
}, [worker, previewWorker]);

return {
isLoading,
service: worker ? { ...worker, ...draftWorker } : null,
preview: (...args) => {
// updatePreview is debounced, so call setPreviewHash outside of it
setPreviewHash(undefined);
setPreviewError(undefined);
void updatePreview(...args);
previewService: worker ? { ...worker, ...previewWorker } : null,
preview: () => {
if (previewWorker !== draftWorker) {
setPreviewHash(undefined);
setPreviewWorker(draftWorker);
}
},
updateDraft: setDraftWorker,
devtoolsUrl,
previewHash,
isPreviewUpdating,
Expand Down
Loading
Loading