Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
cb800d3
feat(laboratory/preflight-sandbox): allow appending headers
jasonkuhrt Jan 16, 2025
fac4434
typo fix
jasonkuhrt Jan 17, 2025
4084026
remove if
jasonkuhrt Jan 20, 2025
0c6d465
expose headers
jasonkuhrt Jan 20, 2025
a21ea80
finish refactor: connected event type defs
jasonkuhrt Jan 20, 2025
cd12274
no substitute headers preflight script
jasonkuhrt Jan 20, 2025
fc2f8c9
embrace headers limitation for now
jasonkuhrt Jan 20, 2025
b5f21b0
no type-encoding of passthrough
jasonkuhrt Jan 20, 2025
e24df4d
fix
jasonkuhrt Jan 20, 2025
fbe7eee
mention todo
jasonkuhrt Jan 20, 2025
998c566
make noop cases debuggable
jasonkuhrt Jan 20, 2025
28b8bbb
kit json decode safe
jasonkuhrt Jan 20, 2025
557bc4e
wip
jasonkuhrt Jan 21, 2025
6db0e1c
Merge branch 'main' into feat/laboratory/preflight-script-append-headers
jasonkuhrt Jan 21, 2025
52222f0
wip
jasonkuhrt Jan 21, 2025
1212fc8
Revert "wip"
jasonkuhrt Jan 21, 2025
c890264
Revert "wip"
jasonkuhrt Jan 21, 2025
04a96fd
work
jasonkuhrt Jan 21, 2025
0b79e9f
simplify
jasonkuhrt Jan 21, 2025
09f15c6
refactor
jasonkuhrt Jan 21, 2025
e3c253b
use const
jasonkuhrt Jan 21, 2025
15b7666
use const
jasonkuhrt Jan 21, 2025
2622f99
reduce diff
jasonkuhrt Jan 21, 2025
525cb4e
refactor
jasonkuhrt Jan 21, 2025
3541df7
untodo
jasonkuhrt Jan 21, 2025
dfce1c4
explicit string
jasonkuhrt Jan 21, 2025
c8a62a4
remove unused code
jasonkuhrt Jan 21, 2025
231f7ad
Merge branch 'main' into feat/laboratory/preflight-script-append-headers
jasonkuhrt Jan 21, 2025
8f36d86
use existing json module
jasonkuhrt Jan 21, 2025
f806027
fix: json primitive value
jasonkuhrt Jan 21, 2025
a9dbd2a
move jsdoc
jasonkuhrt Jan 21, 2025
51290a6
no issue
jasonkuhrt Jan 21, 2025
9c13ef6
explain merge strategy
jasonkuhrt Jan 21, 2025
14329fc
tweak jsdoc
jasonkuhrt Jan 21, 2025
4e0d1df
tweak jsdoc
jasonkuhrt Jan 21, 2025
c963e46
Merge branch 'main' into feat/laboratory/preflight-script-append-headers
jasonkuhrt Jan 22, 2025
960714e
lint
jasonkuhrt Jan 22, 2025
f2e2ba8
test todos
jasonkuhrt Jan 22, 2025
e3692b8
first test
jasonkuhrt Jan 22, 2025
285bf4d
found bug
jasonkuhrt Jan 22, 2025
d6bf9ad
lint
jasonkuhrt Jan 22, 2025
51308fb
refactor
jasonkuhrt Jan 22, 2025
8c3606a
finish tests
jasonkuhrt Jan 22, 2025
8e949f6
refactor
jasonkuhrt Jan 22, 2025
07b24e8
changelog
jasonkuhrt Jan 22, 2025
9ccb39f
docs
jasonkuhrt Jan 22, 2025
5061b30
no only
jasonkuhrt Jan 23, 2025
6f42223
todo done
jasonkuhrt Jan 23, 2025
8c75db9
Merge branch 'main' into feat/laboratory/preflight-script-append-headers
jasonkuhrt Jan 23, 2025
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
14 changes: 14 additions & 0 deletions packages/web/app/src/lib/kit/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export namespace Headers {

Check failure on line 1 in packages/web/app/src/lib/kit/headers.ts

View workflow job for this annotation

GitHub Actions / code-style / eslint-and-prettier

ES2015 module syntax is preferred over namespaces
/**
* Take given HeadersInit and append it (mutating) into given Headers.
*
* @param headers - The Headers object to append to.
* @param headersInit - The HeadersInit object to append from.
*/
export const appendInit = (headers: Headers, headersInit: HeadersInit): void => {
const newHeaders = new globalThis.Headers(headersInit);
newHeaders.forEach((value, key) => {

Check failure on line 10 in packages/web/app/src/lib/kit/headers.ts

View workflow job for this annotation

GitHub Actions / code-style / eslint-and-prettier

Use `for…of` instead of `.forEach(…)`
headers.append(key, value);
});
};
}
5 changes: 5 additions & 0 deletions packages/web/app/src/lib/kit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * as Kit from './index';

Check failure on line 1 in packages/web/app/src/lib/kit/index.ts

View workflow job for this annotation

GitHub Actions / code-style / eslint-and-prettier

Module imports itself

export * from './never';
export * from './headers';
export * from './types/json';
16 changes: 16 additions & 0 deletions packages/web/app/src/lib/kit/never.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* This case is impossible.
* If it is, then there is a bug in our code.
*/
export const neverCase = (value: never): never => {
never(`Unhandled case: ${String(value)}`);
};

/**
* This code cannot be reached.
* If it can be, then there is a bug in our code.
*/
export const never: (contextMessage?: string) => never = contextMessage => {
contextMessage = contextMessage ?? '(no additional context provided)';

Check failure on line 14 in packages/web/app/src/lib/kit/never.ts

View workflow job for this annotation

GitHub Actions / code-style / eslint-and-prettier

Assignment (=) can be replaced with operator assignment (??=)
throw new Error(`Something that should be impossible happened: ${contextMessage}`);
};
19 changes: 19 additions & 0 deletions packages/web/app/src/lib/kit/types/json.ts
Copy link
Member Author

@jasonkuhrt jasonkuhrt Jan 21, 2025

Choose a reason for hiding this comment

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

I have only just discovered https://github.com/graphql-hive/console/blob/231f7ad4af64342efde5cc14fb1a4baf73f9a6fc/packages/web/app/src/lib/preflight-sandbox/json.ts

I will refactor this PR to use that instead of the one I introduced in the Kit namespace.

Also though, I propose the value of a centralized place for easily discoverable standard library like functionality. Kit aims to move towards such a thing. It might help reduce situations like this where an existing abstraction is missed by a colleague whom goes on to recreate it in their own PR later.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export namespace JSON {

Check failure on line 1 in packages/web/app/src/lib/kit/types/json.ts

View workflow job for this annotation

GitHub Actions / code-style / eslint-and-prettier

ES2015 module syntax is preferred over namespaces
export const encode = <value extends Value>(value: value): string => {
return globalThis.JSON.stringify(value);
};

export const encodePretty = <value extends Value>(value: value): string => {
return globalThis.JSON.stringify(value, null, 2);
};

export const decode = (value: string): Value => {
return globalThis.JSON.parse(value);
};

export type Value = PrimitiveValue | NonPrimitiveValue;

export type NonPrimitiveValue = { [key: string]: Value } | Array<Value>;

export type PrimitiveValue = string | number | boolean | null;
}
91 changes: 67 additions & 24 deletions packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ import {
TriangleRightIcon,
} from '@radix-ui/react-icons';
import { useParams } from '@tanstack/react-router';
import { Kit } from '../kit';
import { cn } from '../utils';
import type { LogMessage } from './preflight-script-worker';
import { IFrameEvents } from './shared-types';

type Result = Omit<IFrameEvents.Outgoing.ResultEventData, 'type' | 'runId'>;

export const preflightScriptPlugin: GraphiQLPlugin = {
icon: () => (
<svg
Expand Down Expand Up @@ -141,9 +144,11 @@ const PreflightScript_TargetFragment = graphql(`

type LogRecord = LogMessage | { type: 'separator' };

function safeParseJSON(str: string): Record<string, unknown> | null {
function safeParseJSON<$UnsafeCast extends Kit.JSON.Value = Kit.JSON.Value>(
str: string,
): $UnsafeCast | null {
try {
return JSON.parse(str);
return Kit.JSON.decode(str) as $UnsafeCast;
} catch {
return null;
}
Expand All @@ -165,23 +170,47 @@ export function usePreflightScript(args: {
'hive:laboratory:isPreflightScriptEnabled',
false,
);
const [environmentVariables, setEnvironmentVariables] = useLocalStorage(
'hive:laboratory:environment',
'',
);

// ------------
// Result State
// ------------
//
// todo: Probably better to store the result as a single JSON object value.
// Use a proper versioned schema with codecs for coercing, defaults, validation, etc.
//
// todo: Improve `useLocalStorage` by allowing passing a codec? Then we can co-locate
// the codec with the data and have it applied transparently, use a decoded value
// for the default, etc. ?

const [environmentVariables, setEnvironmentVariables] = useLocalStorage('hive:laboratory:environment', '{}'); // prettier-ignore
const latestEnvironmentVariablesRef = useRef(environmentVariables);
useEffect(() => {
latestEnvironmentVariablesRef.current = environmentVariables;
});
useEffect(() => { latestEnvironmentVariablesRef.current = environmentVariables; }); // prettier-ignore
const decodeEnvironmentVariables = (encoded: string) => safeParseJSON<Result['environmentVariables']>(encoded) ?? {}; // prettier-ignore

const [headers, setHeaders] = useLocalStorage('hive:laboratory:headers', '[]');
const latestHeadersRef = useRef(headers);
useEffect(() => { latestHeadersRef.current = headers; }); // prettier-ignore
const decodeHeaders = (encoded: string) => safeParseJSON<Result['headers']>(encoded) ?? [];

const decodeResult = (): Result => {
return {
environmentVariables: decodeEnvironmentVariables(latestEnvironmentVariablesRef.current), // prettier-ignore
headers: decodeHeaders(latestHeadersRef.current),
};
};
// -----------

const [state, setState] = useState<PreflightWorkerState>(PreflightWorkerState.ready);
const [logs, setLogs] = useState<LogRecord[]>([]);

const currentRun = useRef<null | Function>(null);

async function execute(script = target?.preflightScript?.sourceCode ?? '', isPreview = false) {
async function execute(
script = target?.preflightScript?.sourceCode ?? '',
isPreview = false,
): Promise<Result> {
if (isPreview === false && !isPreflightScriptEnabled) {
return safeParseJSON(latestEnvironmentVariablesRef.current);
return decodeResult();
}

const id = crypto.randomUUID();
Expand All @@ -201,7 +230,7 @@ export function usePreflightScript(args: {
type: IFrameEvents.Incoming.Event.run,
id,
script,
environmentVariables: (environmentVariables && safeParseJSON(environmentVariables)) || {},
environmentVariables: decodeEnvironmentVariables(environmentVariables),
} satisfies IFrameEvents.Incoming.EventData,
'*',
);
Expand Down Expand Up @@ -257,16 +286,20 @@ export function usePreflightScript(args: {
}

if (ev.data.type === IFrameEvents.Outgoing.Event.result) {
const mergedEnvironmentVariables = JSON.stringify(
{
...safeParseJSON(latestEnvironmentVariablesRef.current),
...ev.data.environmentVariables,
},
null,
2,
);
setEnvironmentVariables(mergedEnvironmentVariables);
latestEnvironmentVariablesRef.current = mergedEnvironmentVariables;
const mergedEnvironmentVariablesEncoded = Kit.JSON.encodePretty({
...decodeEnvironmentVariables(latestEnvironmentVariablesRef.current),
...ev.data.environmentVariables,
});
setEnvironmentVariables(mergedEnvironmentVariablesEncoded);
latestEnvironmentVariablesRef.current = mergedEnvironmentVariablesEncoded;

const mergedHeadersEncoded = Kit.JSON.encodePretty([
...decodeHeaders(latestHeadersRef.current),
...ev.data.headers,
]);
setHeaders(mergedHeadersEncoded);
latestHeadersRef.current = mergedHeadersEncoded;

setLogs(logs => [
...logs,
`> End running script. Done in ${(Date.now() - now) / 1000}s`,
Expand Down Expand Up @@ -299,6 +332,16 @@ export function usePreflightScript(args: {
setLogs(logs => [...logs, log]);
return;
}

if (ev.data.type === IFrameEvents.Outgoing.Event.ready) {
return;
}

if (ev.data.type === IFrameEvents.Outgoing.Event.start) {
return;
}

Kit.neverCase(ev.data);
}

window.addEventListener('message', eventHandler);
Expand All @@ -317,7 +360,7 @@ export function usePreflightScript(args: {
window.removeEventListener('message', eventHandler);

setState(PreflightWorkerState.ready);
return safeParseJSON(latestEnvironmentVariablesRef.current);
return decodeResult();
} catch (err) {
if (err instanceof Error) {
setLogs(prev => [
Expand All @@ -329,7 +372,7 @@ export function usePreflightScript(args: {
},
]);
setState(PreflightWorkerState.ready);
return safeParseJSON(latestEnvironmentVariablesRef.current);
return decodeResult();
}
throw err;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import CryptoJS from 'crypto-js';
import CryptoJSPackageJson from 'crypto-js/package.json';
import { Kit } from '../kit';
import { ALLOWED_GLOBALS } from './allowed-globals';
import { isJSONPrimitive } from './json';
import { WorkerEvents } from './shared-types';

interface WorkerData {
headers: Headers;
environmentVariables: Record<string, unknown>;
}

export type LogMessage = string | Error;

/**
Expand Down Expand Up @@ -47,11 +53,14 @@ async function execute(args: WorkerEvents.Incoming.EventData): Promise<void> {
return;
}

const { environmentVariables, script } = args;
const { script } = args;

// When running in worker `environmentVariables` will not be a reference to the main thread value
// but sometimes this will be tested outside the worker, so we don't want to mutate the input in that case
const workingEnvironmentVariables = { ...environmentVariables };
const workerData: WorkerData = {
headers: new Headers(),
// When running in worker `environmentVariables` will not be a reference to the main thread value
// but sometimes this will be tested outside the worker, so we don't want to mutate the input in that case
environmentVariables: { ...args.environmentVariables },
};

// generate list of all in scope variables, we do getOwnPropertyNames and `for in` because each contain slightly different sets of keys
const allGlobalKeys = Object.getOwnPropertyNames(globalThis);
Expand Down Expand Up @@ -116,17 +125,44 @@ async function execute(args: WorkerEvents.Incoming.EventData): Promise<void> {
},
environment: {
get(key: string) {
return Object.freeze(workingEnvironmentVariables[key]);
return Object.freeze(workerData.environmentVariables[key]);
},
set(key: string, value: unknown) {
const validValue = getValidEnvVariable(value);
if (validValue === undefined) {
delete workingEnvironmentVariables[key];
delete workerData.environmentVariables[key];
} else {
workingEnvironmentVariables[key] = validValue;
workerData.environmentVariables[key] = validValue;
}
},
},
/**
* Helpers for manipulating the request before it is sent.
*/
request: {
/**
* Helpers for manipulating the request headers.
*/
headers: {
/**
* Add one header to the request.
*
* @param header - The name of the header.
* @param value - The value of the header.
*/
add: (header: string, value: string) => {
workerData.headers.append(header, value);
},
/**
* Add multiple headers to the request.
*
* @param headersInit - The {@link HeadersInit} to add.
*/
addMany: (headersInit: HeadersInit) => {
Kit.Headers.appendInit(workerData.headers, headersInit);
},
},
},
/**
* Mimics the `prompt` function in the browser, by sending a message to the main thread
* and waiting for a response.
Expand Down Expand Up @@ -167,9 +203,12 @@ ${script}})()`;
sendMessage({ type: WorkerEvents.Outgoing.Event.error, error: error as Error });
return;
}

sendMessage({
type: WorkerEvents.Outgoing.Event.result,
environmentVariables: workingEnvironmentVariables,
// todo: We need to more precisely type environment value. Currently unknown. Why?
environmentVariables: workerData.environmentVariables as any,
headers: Array.from(workerData.headers.entries()),
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Kit } from '../kit';
import PreflightWorker from './preflight-script-worker?worker&inline';
import { IFrameEvents, WorkerEvents } from './shared-types';

Expand Down Expand Up @@ -103,9 +104,9 @@ function handleEvent(data: IFrameEvents.Incoming.EventData) {

if (ev.data.type === WorkerEvents.Outgoing.Event.result) {
postMessage({
...ev.data,
type: IFrameEvents.Outgoing.Event.result,
runId,
environmentVariables: ev.data.environmentVariables,
});
terminate();
return;
Expand All @@ -129,6 +130,8 @@ function handleEvent(data: IFrameEvents.Incoming.EventData) {
terminate();
return;
}

Kit.neverCase(ev.data);
},
);

Expand Down
Loading
Loading