@webwriter/code
Version:
Write and run code as a code cell. Supports several languages (HTML, JavaScript/TypeScript, Python, Java, WebAssembly).
182 lines (159 loc) • 6.32 kB
text/typescript
//#region Types
// Code execution
export type ExecuteCodeMessage = {
type: "execute";
executionId: string;
code: string;
};
export type ExecutionStatusMessage = {
type: "executionStatus";
executionId: string;
status: "running" | "terminated";
};
// Logging
export type SerializedNull = { type: "null" };
export type SerializedUndefined = { type: "undefined" };
export type SerializedString = { type: "string"; value: string };
export type SerializedNumber = { type: "number"; value: number };
export type SerializedBoolean = { type: "boolean"; value: boolean };
export type SerializedBigInt = { type: "bigint"; value: string };
export type SerializedSymbol = { type: "symbol"; description: string };
export type SerializedFunction = { type: "function"; value: string };
export type SerializedArray = {
type: "array";
length: number;
value: { realized: true; values: SerializedValue[]; expanded: boolean } | { realized: false; objectId: number };
};
export type SerializedObject = {
type: "object";
empty: boolean;
value:
| { realized: true; properties: { key: string; value: SerializedValue }[]; expanded: boolean }
| { realized: false; objectId: number };
};
export type SerializedValue =
| SerializedNull
| SerializedUndefined
| SerializedString
| SerializedNumber
| SerializedBoolean
| SerializedBigInt
| SerializedSymbol
| SerializedFunction
| SerializedArray
| SerializedObject;
export type LogMessage = {
type: "log";
executionId: string;
label: "debug" | "info" | "warning" | "error";
logs: SerializedValue[];
};
export type ConsoleClearMessage = {
type: "consoleClear";
};
export type RealizeObjectMessage = {
type: "realizeObject";
objectId: number;
};
export type RealizedObjectMessage = {
type: "realizedObject";
objectId: number;
value: SerializedObject | SerializedArray;
};
export type ToWorkerMessage = ExecuteCodeMessage | RealizeObjectMessage;
export type FromWorkerMessage = ExecutionStatusMessage | LogMessage | ConsoleClearMessage | RealizedObjectMessage;
//#endregion
const objectCache = new Map<number, any>();
let objectIdCounter = 0;
function serializeValue(value: any, resolveNested: boolean = false): SerializedValue {
// Handle all primitive types first
if (value === null) return { type: "null" };
if (value === undefined) return { type: "undefined" };
if (typeof value === "string") return { type: "string", value };
if (typeof value === "number") return { type: "number", value };
if (typeof value === "boolean") return { type: "boolean", value };
if (typeof value === "bigint") return { type: "bigint", value: value.toString() };
if (typeof value === "symbol") return { type: "symbol", description: value.description || "" };
if (typeof value === "function") return { type: "function", value: value.toString() };
if (value instanceof Error) {
return { type: "string", value: value.message ?? value + "" };
}
// Rewrite "special" object to array or object
if (value instanceof Map) value = Object.fromEntries(value.entries());
if (value instanceof Set) value = Array.from(value);
if (value instanceof Date) return { type: "string", value: value.toISOString() };
if (value instanceof RegExp) return { type: "string", value: value.toString() };
let objectId = -1;
if (!resolveNested) {
objectId = objectIdCounter++;
objectCache.set(objectId, value);
}
if (Array.isArray(value)) {
return {
type: "array",
length: value.length,
value: resolveNested
? { realized: true, values: value.map((item) => serializeValue(item, false)), expanded: false }
: { realized: false, objectId },
};
}
return {
type: "object",
empty: Object.keys(value).length === 0,
value: resolveNested
? {
realized: true,
properties: Object.entries(value).map(([key, val]) => ({
key,
value: serializeValue(val, false),
})),
expanded: false,
}
: { realized: false, objectId },
};
}
const oldConsole = console;
self.onmessage = async (event) => {
const message = event.data as ToWorkerMessage;
if (message.type === "execute") {
const { executionId, code } = message;
function sendLog(label: string, ...args: any[]) {
const serializedArgs = args.map((arg) => serializeValue(arg, true));
self.postMessage({
type: "log",
executionId,
label,
logs: serializedArgs,
} as LogMessage);
}
self.postMessage({ type: "executionStatus", executionId, status: "running" } as ExecutionStatusMessage);
try {
console = {
log: (...args: any[]) => sendLog("info", ...args),
warn: (...args: any[]) => sendLog("warning", ...args),
error: (...args: any[]) => sendLog("error", ...args),
info: (...args: any[]) => sendLog("debug", ...args),
clear: () => self.postMessage({ type: "consoleClear" } as ConsoleClearMessage),
} as any;
await new Function(code).bind({})();
self.postMessage({ type: "executionStatus", executionId, status: "terminated" } as ExecutionStatusMessage);
} catch (error) {
sendLog("error", error);
self.postMessage({ type: "executionStatus", executionId, status: "terminated" } as ExecutionStatusMessage);
}
} else if (message.type === "realizeObject") {
const { objectId } = message;
const value = objectCache.get(objectId);
if (value !== undefined) {
self.postMessage({
type: "realizedObject",
objectId,
value: serializeValue(value, true),
} as RealizedObjectMessage);
} else {
oldConsole.error(`Object with ID ${objectId} not found in cache.`);
}
} else {
oldConsole.error("Unknown message type:", event.data.type);
}
};