PlugOS refactor and other tweaks (#631)
* Prep for in-process plug loading (e.g. for CF workers, Deno Deploy) * Prototype of fixed in-process loading plugs * Fix: buttons not to scroll with content * Better positioning of modal especially on mobile * Move query caching outside query * Fix annoying mouse behavior when filter box appears * Page navigator search tweaks
This commit is contained in:
@@ -1,26 +1,26 @@
|
||||
import { WorkerSandbox } from "./worker_sandbox.ts";
|
||||
import { Plug } from "../plug.ts";
|
||||
import { Sandbox } from "./sandbox.ts";
|
||||
import type { SandboxFactory } from "./sandbox.ts";
|
||||
|
||||
// Uses Deno's permissions to lock the worker down significantly
|
||||
export function createSandbox<HookT>(plug: Plug<HookT>): Sandbox<HookT> {
|
||||
return new WorkerSandbox(plug, {
|
||||
deno: {
|
||||
permissions: {
|
||||
// Allow network access
|
||||
net: true,
|
||||
// This is required for console logging to work, apparently?
|
||||
env: true,
|
||||
// No talking to native code
|
||||
ffi: false,
|
||||
// No invocation of shell commands
|
||||
run: false,
|
||||
// No read access to the file system
|
||||
read: false,
|
||||
// No write access to the file system
|
||||
write: false,
|
||||
export function createSandbox<HookT>(workerUrl: URL): SandboxFactory<HookT> {
|
||||
return (plug) =>
|
||||
new WorkerSandbox(plug, workerUrl, {
|
||||
deno: {
|
||||
permissions: {
|
||||
// Allow network access
|
||||
net: true,
|
||||
// This is required for console logging to work, apparently?
|
||||
env: true,
|
||||
// No talking to native code
|
||||
ffi: false,
|
||||
// No invocation of shell commands
|
||||
run: false,
|
||||
// No read access to the file system
|
||||
read: false,
|
||||
// No write access to the file system
|
||||
write: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Have to do this because the "deno" option is not standard and doesn't typecheck yet
|
||||
} as any);
|
||||
// Have to do this because the "deno" option is not standard and doesn't typecheck yet
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,38 @@ import { PromiseQueue } from "$sb/lib/async.ts";
|
||||
import { Plug } from "../plug.ts";
|
||||
import { Sandbox } from "./sandbox.ts";
|
||||
import { Manifest } from "../types.ts";
|
||||
import { System } from "../system.ts";
|
||||
import { SandboxFactory } from "./sandbox.ts";
|
||||
|
||||
/**
|
||||
* This implements a "no sandbox" sandbox that actually runs code the main thread, without any isolation.
|
||||
* This is useful for (often serverless) environments like CloudFlare workers and Deno Deploy that do not support workers.
|
||||
* Since these environments often also don't allow dynamic loading (or even eval'ing) of code, plug code needs to be
|
||||
* imported as a regular ESM module (which is possible).
|
||||
*
|
||||
* To make this work, a global `syscall` function needs to be injected into the global scope.
|
||||
* Since a syscall relies on a System, we need to track the active System in a global variable.
|
||||
* The issue with this is that it means that only a single System can be active at a given time per JS process.
|
||||
* To enforce this, we have a runWithSystemLock function that can be used to run code in a System-locked context, effectively queuing the execution of tasks sequentially.
|
||||
* This isn't great, but it's the best we can do.
|
||||
*
|
||||
* Luckily, in the only contexts in which you need to run plugs this way are serverless, where code will be
|
||||
* run in a bunch of isolates with hopefully low parallelism of requests per isolate.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A type representing the `plug` export of a plug, used via e.g. `import { plug } from "./some.plug.js`
|
||||
* Values of this type are passed into the `noSandboxFactory` function when called on a system.load
|
||||
*/
|
||||
export type PlugExport = {
|
||||
manifest: Manifest<any>;
|
||||
functionMapping: Record<string, (...args: any[]) => any>;
|
||||
};
|
||||
|
||||
// The global variable tracking the currently active system (if any)
|
||||
let activeSystem:
|
||||
| System<any>
|
||||
| undefined;
|
||||
|
||||
// We need to hard inject the syscall function into the global scope
|
||||
declare global {
|
||||
@@ -9,60 +41,76 @@ declare global {
|
||||
syscall(name: string, ...args: any[]): Promise<any>;
|
||||
}
|
||||
}
|
||||
|
||||
export type PlugExport<HookT> = {
|
||||
manifest: Manifest<HookT>;
|
||||
functionMapping: Record<string, Function>;
|
||||
};
|
||||
|
||||
const functionQueue = new PromiseQueue();
|
||||
|
||||
let activePlug: Plug<any> | undefined;
|
||||
|
||||
// @ts-ignore: globalThis
|
||||
globalThis.syscall = (name: string, ...args: any[]): Promise<any> => {
|
||||
if (!activePlug) {
|
||||
throw new Error("No active plug");
|
||||
if (!activeSystem) {
|
||||
throw new Error(`No currently active system, can't invoke syscall ${name}`);
|
||||
}
|
||||
console.log("Calling syscall", name, args);
|
||||
return activePlug.syscall(name, args);
|
||||
// Invoke syscall with no active plug set (because we don't know which plug is invoking the syscall)
|
||||
return activeSystem.syscall({}, name, args);
|
||||
};
|
||||
|
||||
// Global sequential task queue for running tasks in a System-locked context
|
||||
const taskQueue = new PromiseQueue();
|
||||
|
||||
/**
|
||||
* Schedules a task to run in a System-locked context
|
||||
* in effect this will ensure only one such context is active at a given time allowing for no parallelism
|
||||
* @param system to activate while running the task
|
||||
* @param task callback to run
|
||||
* @returns the result of the task once it completes
|
||||
*/
|
||||
export function runWithSystemLock(
|
||||
system: System<any>,
|
||||
task: () => Promise<any>,
|
||||
): Promise<any> {
|
||||
return taskQueue.runInQueue(async () => {
|
||||
// Set the global active system, which is used by the syscall function
|
||||
activeSystem = system;
|
||||
try {
|
||||
// Run the logic, note putting the await here is crucial to make sure the `finally` block runs at the right time
|
||||
return await task();
|
||||
} finally {
|
||||
// And then reset the global active system whether the thing blew up or not
|
||||
activeSystem = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a no-sandbox sandbox that runs code in the main thread
|
||||
*/
|
||||
export class NoSandbox<HookT> implements Sandbox<HookT> {
|
||||
manifest?: Manifest<HookT> | undefined;
|
||||
manifest: Manifest<HookT>;
|
||||
|
||||
constructor(
|
||||
private plug: Plug<HookT>,
|
||||
private plugExport: PlugExport<HookT>,
|
||||
readonly plug: Plug<HookT>,
|
||||
readonly plugExport: PlugExport,
|
||||
) {
|
||||
this.manifest = plugExport.manifest;
|
||||
plug.manifest = this.manifest;
|
||||
}
|
||||
|
||||
init(): Promise<void> {
|
||||
// Nothing to do
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
invoke(name: string, args: any[]): Promise<any> {
|
||||
activePlug = this.plug;
|
||||
return functionQueue.runInQueue(async () => {
|
||||
try {
|
||||
const fn = this.plugExport.functionMapping[name];
|
||||
if (!fn) {
|
||||
throw new Error(`Function not loaded: ${name}`);
|
||||
}
|
||||
return await fn(...args);
|
||||
} finally {
|
||||
activePlug = undefined;
|
||||
}
|
||||
});
|
||||
const fn = this.plugExport.functionMapping[name];
|
||||
if (!fn) {
|
||||
throw new Error(`Function not defined: ${name}`);
|
||||
}
|
||||
return Promise.resolve(fn(...args));
|
||||
}
|
||||
|
||||
stop() {
|
||||
// Nothing to do
|
||||
}
|
||||
}
|
||||
|
||||
export function noSandboxFactory<HookT>(
|
||||
plugExport: PlugExport<HookT>,
|
||||
): (plug: Plug<HookT>) => Sandbox<HookT> {
|
||||
return (plug: Plug<HookT>) => new NoSandbox(plug, plugExport);
|
||||
export function createSandbox<HookT>(
|
||||
plugExport: PlugExport,
|
||||
): SandboxFactory<HookT> {
|
||||
return (plug: Plug<any>) => new NoSandbox(plug, plugExport);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WorkerSandbox } from "./worker_sandbox.ts";
|
||||
import type { Plug } from "../plug.ts";
|
||||
import { Sandbox } from "./sandbox.ts";
|
||||
import type { SandboxFactory } from "./sandbox.ts";
|
||||
|
||||
export function createSandbox<HookT>(plug: Plug<HookT>): Sandbox<HookT> {
|
||||
return new WorkerSandbox(plug);
|
||||
export function createSandbox<HookT>(workerUrl: URL): SandboxFactory<HookT> {
|
||||
return (plug: Plug<HookT>) => new WorkerSandbox(plug, workerUrl);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
|
||||
|
||||
constructor(
|
||||
readonly plug: Plug<HookT>,
|
||||
public workerUrl: URL,
|
||||
private workerOptions = {},
|
||||
) {
|
||||
}
|
||||
@@ -35,7 +36,7 @@ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
|
||||
console.warn("Double init of sandbox, ignoring");
|
||||
return Promise.resolve();
|
||||
}
|
||||
this.worker = new Worker(this.plug.workerUrl!, {
|
||||
this.worker = new Worker(this.workerUrl, {
|
||||
...this.workerOptions,
|
||||
type: "module",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user