Massive cleanup and plugbox cleanup
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
import { System } from "./runtime";
|
||||
import { CronHook } from "./types";
|
||||
import cron from "node-cron";
|
||||
|
||||
export function cronSystem(system: System<CronHook>) {
|
||||
let task = cron.schedule("* * * * *", () => {
|
||||
|
||||
});
|
||||
// @ts-ignore
|
||||
task.destroy();
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { System } from "./runtime";
|
||||
import { EndpointHook } from "./types";
|
||||
import express from "express";
|
||||
|
||||
export type EndpointRequest = {
|
||||
method: string;
|
||||
path: string;
|
||||
query: { [key: string]: string };
|
||||
headers: { [key: string]: string };
|
||||
body: any;
|
||||
};
|
||||
|
||||
export type EndpointResponse = {
|
||||
status: number;
|
||||
headers?: { [key: string]: string };
|
||||
body: any;
|
||||
};
|
||||
|
||||
const endPointPrefix = "/_";
|
||||
|
||||
export function exposeSystem(system: System<EndpointHook>) {
|
||||
return (
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
next: express.NextFunction
|
||||
) => {
|
||||
if (!req.path.startsWith(endPointPrefix)) {
|
||||
return next();
|
||||
}
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
for (const [plugName, plug] of system.loadedPlugs.entries()) {
|
||||
const manifest = plug.manifest;
|
||||
if (!manifest) {
|
||||
continue;
|
||||
}
|
||||
const endpoints = manifest.hooks?.endpoints;
|
||||
if (endpoints) {
|
||||
let prefix = `${endPointPrefix}/${plugName}`;
|
||||
if (!req.path.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
for (const { path, method, handler } of endpoints) {
|
||||
let prefixedPath = `${prefix}${path}`;
|
||||
if (prefixedPath === req.path && method === req.method) {
|
||||
try {
|
||||
const response: EndpointResponse = await plug.invoke(
|
||||
handler,
|
||||
[
|
||||
{
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
headers: req.headers,
|
||||
} as EndpointRequest,
|
||||
]
|
||||
);
|
||||
let resp = res.status(response.status);
|
||||
if (response.headers) {
|
||||
for (const [key, value] of Object.entries(
|
||||
response.headers
|
||||
)) {
|
||||
resp = resp.header(key, value);
|
||||
}
|
||||
}
|
||||
resp.send(response.body);
|
||||
return;
|
||||
} catch (e: any) {
|
||||
console.error("Error executing function", e);
|
||||
res.status(500).send(e.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
next(e);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ControllerMessage, WorkerLike, WorkerMessage } from "./types";
|
||||
import { Sandbox, System } from "./runtime";
|
||||
import { safeRun } from "./util";
|
||||
import { safeRun } from "../util";
|
||||
|
||||
// @ts-ignore
|
||||
import sandboxHtml from "bundle-text:./iframe_sandbox.html";
|
||||
import { Sandbox } from "../sandbox";
|
||||
import { System } from "../system";
|
||||
import { WorkerLike } from "./worker";
|
||||
|
||||
class IFrameWrapper implements WorkerLike {
|
||||
private iframe: HTMLIFrameElement;
|
||||
@@ -1,12 +1,11 @@
|
||||
import { ControllerMessage, WorkerLike, WorkerMessage } from "./types";
|
||||
import { System, Sandbox } from "./runtime";
|
||||
|
||||
import { Worker } from "worker_threads";
|
||||
import * as fs from "fs";
|
||||
import { safeRun } from "./util";
|
||||
import { safeRun } from "../util";
|
||||
|
||||
// @ts-ignore
|
||||
import workerCode from "bundle-text:./node_worker.ts";
|
||||
import { Sandbox } from "../sandbox";
|
||||
import { System } from "../system";
|
||||
import { WorkerLike } from "./worker";
|
||||
|
||||
class NodeWorkerWrapper implements WorkerLike {
|
||||
onMessage?: (message: any) => Promise<void>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ControllerMessage, WorkerMessage, WorkerMessageType } from "./types";
|
||||
import { safeRun } from "./util";
|
||||
import { safeRun } from "../util";
|
||||
import { ControllerMessage, WorkerMessage } from "./worker";
|
||||
|
||||
let loadedFunctions = new Map<string, Function>();
|
||||
let pendingRequests = new Map<
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ControllerMessage, WorkerLike, WorkerMessage } from "./types";
|
||||
import { Sandbox, System } from "./runtime";
|
||||
import { safeRun } from "./util";
|
||||
import { safeRun } from "../util";
|
||||
import { Sandbox } from "../sandbox";
|
||||
import { System } from "../system";
|
||||
import { WorkerLike } from "./worker";
|
||||
|
||||
class WebWorkerWrapper implements WorkerLike {
|
||||
private worker: Worker;
|
||||
@@ -0,0 +1,30 @@
|
||||
export type ControllerMessageType = "inited" | "result" | "syscall";
|
||||
|
||||
export type ControllerMessage = {
|
||||
type: ControllerMessageType;
|
||||
id?: number;
|
||||
name?: string;
|
||||
args?: any[];
|
||||
error?: string;
|
||||
result?: any;
|
||||
};
|
||||
|
||||
export interface WorkerLike {
|
||||
ready: Promise<void>;
|
||||
onMessage?: (message: any) => Promise<void>;
|
||||
|
||||
postMessage(message: any): void;
|
||||
|
||||
terminate(): void;
|
||||
}
|
||||
|
||||
export type WorkerMessageType = "load" | "invoke" | "syscall-response";
|
||||
export type WorkerMessage = {
|
||||
type: WorkerMessageType;
|
||||
id?: number;
|
||||
name?: string;
|
||||
code?: string;
|
||||
args?: any[];
|
||||
result?: any;
|
||||
error?: any;
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createSandbox } from "./node_sandbox";
|
||||
import { System } from "./runtime";
|
||||
import { test, expect } from "@jest/globals";
|
||||
import { EndPointDef, EndpointHook, Manifest } from "./types";
|
||||
import { createSandbox } from "../environment/node_sandbox";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { Manifest } from "../types";
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import { exposeSystem } from "./endpoints";
|
||||
import { EndpointFeature, EndpointHook } from "./endpoint";
|
||||
import { System } from "../system";
|
||||
|
||||
test("Run a plugbox endpoint server", async () => {
|
||||
let system = new System<EndpointHook>();
|
||||
let system = new System<EndpointHook>("server");
|
||||
let plug = await system.load(
|
||||
"test",
|
||||
{
|
||||
@@ -32,7 +32,9 @@ test("Run a plugbox endpoint server", async () => {
|
||||
|
||||
const app = express();
|
||||
const port = 3123;
|
||||
app.use(exposeSystem(system));
|
||||
|
||||
system.addFeature(new EndpointFeature(app));
|
||||
|
||||
let server = app.listen(port, () => {
|
||||
console.log(`Listening on port ${port}`);
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Feature, Manifest } from "../types";
|
||||
import { Express, NextFunction, Request, Response } from "express";
|
||||
import { System } from "../system";
|
||||
|
||||
export type EndpointRequest = {
|
||||
method: string;
|
||||
path: string;
|
||||
query: { [key: string]: string };
|
||||
headers: { [key: string]: string };
|
||||
body: any;
|
||||
};
|
||||
|
||||
export type EndpointResponse = {
|
||||
status: number;
|
||||
headers?: { [key: string]: string };
|
||||
body: any;
|
||||
};
|
||||
|
||||
export type EndpointHook = {
|
||||
endpoints?: EndPointDef[];
|
||||
};
|
||||
|
||||
export type EndPointDef = {
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS";
|
||||
path: string;
|
||||
handler: string; // function name
|
||||
};
|
||||
|
||||
const endPointPrefix = "/_";
|
||||
|
||||
export class EndpointFeature implements Feature<EndpointHook> {
|
||||
private app: Express;
|
||||
|
||||
constructor(app: Express) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
apply(system: System<EndpointHook>): void {
|
||||
this.app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.path.startsWith(endPointPrefix)) {
|
||||
return next();
|
||||
}
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
for (const [plugName, plug] of system.loadedPlugs.entries()) {
|
||||
const manifest = plug.manifest;
|
||||
if (!manifest) {
|
||||
continue;
|
||||
}
|
||||
const endpoints = manifest.hooks?.endpoints;
|
||||
if (endpoints) {
|
||||
let prefix = `${endPointPrefix}/${plugName}`;
|
||||
if (!req.path.startsWith(prefix)) {
|
||||
continue;
|
||||
}
|
||||
for (const { path, method, handler } of endpoints) {
|
||||
let prefixedPath = `${prefix}${path}`;
|
||||
if (prefixedPath === req.path && method === req.method) {
|
||||
try {
|
||||
const response: EndpointResponse = await plug.invoke(
|
||||
handler,
|
||||
[
|
||||
{
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
headers: req.headers,
|
||||
} as EndpointRequest,
|
||||
]
|
||||
);
|
||||
let resp = res.status(response.status);
|
||||
if (response.headers) {
|
||||
for (const [key, value] of Object.entries(
|
||||
response.headers
|
||||
)) {
|
||||
resp = resp.header(key, value);
|
||||
}
|
||||
}
|
||||
resp.send(response.body);
|
||||
return;
|
||||
} catch (e: any) {
|
||||
console.error("Error executing function", e);
|
||||
res.status(500).send(e.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
next(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<EndpointHook>): string[] {
|
||||
const endpoints = manifest.hooks.endpoints;
|
||||
let errors = [];
|
||||
if (endpoints) {
|
||||
for (let { method, path, handler } of endpoints) {
|
||||
if (!path) {
|
||||
errors.push("Path not defined for endpoint");
|
||||
}
|
||||
if (["GET", "POST", "PUT", "DELETE"].indexOf(method) === -1) {
|
||||
errors.push(
|
||||
`Invalid method ${method} for end point with with ${path}`
|
||||
);
|
||||
}
|
||||
if (!manifest.functions[handler]) {
|
||||
errors.push(`Endpoint handler function ${handler} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Feature, Manifest } from "../types";
|
||||
import cron, { ScheduledTask } from "node-cron";
|
||||
import { safeRun } from "../util";
|
||||
import { System } from "../system";
|
||||
|
||||
export type CronHook = {
|
||||
crons?: CronDef[];
|
||||
};
|
||||
|
||||
export type CronDef = {
|
||||
cron: string;
|
||||
handler: string; // function name
|
||||
};
|
||||
|
||||
export class NodeCronFeature implements Feature<CronHook> {
|
||||
apply(system: System<CronHook>): void {
|
||||
let tasks: ScheduledTask[] = [];
|
||||
system.on({
|
||||
plugLoaded: (name, plug) => {
|
||||
reloadCrons();
|
||||
},
|
||||
plugUnloaded(name, plug) {
|
||||
reloadCrons();
|
||||
},
|
||||
});
|
||||
|
||||
reloadCrons();
|
||||
|
||||
function reloadCrons() {
|
||||
// ts-ignore
|
||||
tasks.forEach((task) => task.stop());
|
||||
tasks = [];
|
||||
for (let plug of system.loadedPlugs.values()) {
|
||||
const crons = plug.manifest?.hooks?.crons;
|
||||
if (crons) {
|
||||
for (let cronDef of crons) {
|
||||
tasks.push(
|
||||
cron.schedule(cronDef.cron, () => {
|
||||
console.log("Now acting on cron", cronDef.cron);
|
||||
safeRun(async () => {
|
||||
try {
|
||||
await plug.invoke(cronDef.handler, []);
|
||||
} catch (e: any) {
|
||||
console.error("Execution of cron function failed", e);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<CronHook>): string[] {
|
||||
const crons = manifest.hooks.crons;
|
||||
let errors = [];
|
||||
if (crons) {
|
||||
for (let cronDef of crons) {
|
||||
if (!cron.validate(cronDef.cron)) {
|
||||
errors.push(`Invalid cron expression ${cronDef.cron}`);
|
||||
}
|
||||
if (!manifest.functions[cronDef.handler]) {
|
||||
errors.push(`Cron handler function ${cronDef.handler} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Manifest, RuntimeEnvironment } from "./types";
|
||||
import { Sandbox } from "./sandbox";
|
||||
import { System } from "./system";
|
||||
|
||||
export class Plug<HookT> {
|
||||
system: System<HookT>;
|
||||
sandbox: Sandbox;
|
||||
public manifest?: Manifest<HookT>;
|
||||
readonly runtimeEnv: RuntimeEnvironment;
|
||||
|
||||
constructor(system: System<HookT>, name: string, sandbox: Sandbox) {
|
||||
this.system = system;
|
||||
this.sandbox = sandbox;
|
||||
this.runtimeEnv = system.runtimeEnv;
|
||||
}
|
||||
|
||||
async load(manifest: Manifest<HookT>) {
|
||||
this.manifest = manifest;
|
||||
await this.dispatchEvent("load");
|
||||
}
|
||||
|
||||
canInvoke(name: string) {
|
||||
if (!this.manifest) {
|
||||
return false;
|
||||
}
|
||||
const funDef = this.manifest.functions[name];
|
||||
if (!funDef) {
|
||||
throw new Error(`Function ${name} not found in manifest`);
|
||||
}
|
||||
return !funDef.env || funDef.env === this.runtimeEnv;
|
||||
}
|
||||
|
||||
async invoke(name: string, args: Array<any>): Promise<any> {
|
||||
if (!this.sandbox.isLoaded(name)) {
|
||||
const funDef = this.manifest!.functions[name];
|
||||
if (!funDef) {
|
||||
throw new Error(`Function ${name} not found in manifest`);
|
||||
}
|
||||
if (!this.canInvoke(name)) {
|
||||
throw new Error(
|
||||
`Function ${name} is not available in ${this.runtimeEnv}`
|
||||
);
|
||||
}
|
||||
await this.sandbox.load(name, funDef.code!);
|
||||
}
|
||||
return await this.sandbox.invoke(name, args);
|
||||
}
|
||||
|
||||
async dispatchEvent(name: string, data?: any): Promise<any[]> {
|
||||
if (!this.manifest!.hooks?.events) {
|
||||
return [];
|
||||
}
|
||||
let functionsToSpawn = this.manifest!.hooks.events[name];
|
||||
if (functionsToSpawn) {
|
||||
return await Promise.all(
|
||||
functionsToSpawn.map((functionToSpawn: string) => {
|
||||
// Only dispatch functions on events when they're allowed to be invoked in this environment
|
||||
if (this.canInvoke(functionToSpawn)) {
|
||||
return this.invoke(functionToSpawn, [data]);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.sandbox.stop();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import fs, { stat, watch } from "fs/promises";
|
||||
import fs, { watch } from "fs/promises";
|
||||
import path from "path";
|
||||
import { createSandbox } from "./node_sandbox";
|
||||
import { System } from "./runtime";
|
||||
import { createSandbox } from "./environment/node_sandbox";
|
||||
import { safeRun } from "../server/util";
|
||||
import { System } from "./system";
|
||||
|
||||
function extractPlugName(localPath: string): string {
|
||||
const baseName = path.basename(localPath);
|
||||
@@ -34,10 +34,8 @@ export class DiskPlugLoader<HookT> {
|
||||
} catch (e) {
|
||||
// Likely removed
|
||||
await this.system.unload(plugName);
|
||||
this.system.emit("plugRemoved", plugName);
|
||||
}
|
||||
const plugDef = await this.loadPlugFromFile(localPath);
|
||||
this.system.emit("plugUpdated", plugName, plugDef);
|
||||
} catch {
|
||||
// ignore, error handled by loadPlug
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createSandbox } from "./node_sandbox";
|
||||
import { System } from "./runtime";
|
||||
import { test, expect } from "@jest/globals";
|
||||
import { createSandbox } from "./environment/node_sandbox";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { System } from "./system";
|
||||
|
||||
test("Run a Node sandbox", async () => {
|
||||
let system = new System();
|
||||
let system = new System("server");
|
||||
system.registerSyscalls({
|
||||
addNumbers: (a, b) => {
|
||||
return a + b;
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
import {
|
||||
ControllerMessage,
|
||||
Manifest,
|
||||
WorkerLike,
|
||||
WorkerMessage,
|
||||
} from "./types";
|
||||
import { EventEmitter } from "../common/event";
|
||||
|
||||
interface SysCallMapping {
|
||||
[key: string]: (...args: any) => Promise<any> | any;
|
||||
}
|
||||
|
||||
export class Sandbox {
|
||||
protected worker: WorkerLike;
|
||||
protected reqId = 0;
|
||||
protected outstandingInits = new Map<string, () => void>();
|
||||
protected outstandingInvocations = new Map<
|
||||
number,
|
||||
{ resolve: (result: any) => void; reject: (e: any) => void }
|
||||
>();
|
||||
protected loadedFunctions = new Set<string>();
|
||||
protected system: System<any>;
|
||||
|
||||
constructor(system: System<any>, worker: WorkerLike) {
|
||||
worker.onMessage = this.onMessage.bind(this);
|
||||
this.worker = worker;
|
||||
this.system = system;
|
||||
}
|
||||
|
||||
isLoaded(name: string) {
|
||||
return this.loadedFunctions.has(name);
|
||||
}
|
||||
|
||||
async load(name: string, code: string): Promise<void> {
|
||||
await this.worker.ready;
|
||||
this.worker.postMessage({
|
||||
type: "load",
|
||||
name: name,
|
||||
code: code,
|
||||
} as WorkerMessage);
|
||||
return new Promise((resolve) => {
|
||||
this.loadedFunctions.add(name);
|
||||
this.outstandingInits.set(name, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
async onMessage(data: ControllerMessage) {
|
||||
switch (data.type) {
|
||||
case "inited":
|
||||
let initCb = this.outstandingInits.get(data.name!);
|
||||
initCb && initCb();
|
||||
this.outstandingInits.delete(data.name!);
|
||||
break;
|
||||
case "syscall":
|
||||
try {
|
||||
let result = await this.system.syscall(data.name!, data.args!);
|
||||
|
||||
this.worker.postMessage({
|
||||
type: "syscall-response",
|
||||
id: data.id,
|
||||
result: result,
|
||||
} as WorkerMessage);
|
||||
} catch (e: any) {
|
||||
this.worker.postMessage({
|
||||
type: "syscall-response",
|
||||
id: data.id,
|
||||
error: e.message,
|
||||
} as WorkerMessage);
|
||||
}
|
||||
break;
|
||||
case "result":
|
||||
let resultCbs = this.outstandingInvocations.get(data.id!);
|
||||
this.outstandingInvocations.delete(data.id!);
|
||||
if (data.error) {
|
||||
resultCbs && resultCbs.reject(new Error(data.error));
|
||||
} else {
|
||||
resultCbs && resultCbs.resolve(data.result);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown message type", data);
|
||||
}
|
||||
}
|
||||
|
||||
async invoke(name: string, args: any[]): Promise<any> {
|
||||
this.reqId++;
|
||||
this.worker.postMessage({
|
||||
type: "invoke",
|
||||
id: this.reqId,
|
||||
name,
|
||||
args,
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
this.outstandingInvocations.set(this.reqId, { resolve, reject });
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.worker.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
export class Plug<HookT> {
|
||||
system: System<HookT>;
|
||||
sandbox: Sandbox;
|
||||
public manifest?: Manifest<HookT>;
|
||||
|
||||
constructor(system: System<HookT>, name: string, sandbox: Sandbox) {
|
||||
this.system = system;
|
||||
this.sandbox = sandbox;
|
||||
}
|
||||
|
||||
async load(manifest: Manifest<HookT>) {
|
||||
this.manifest = manifest;
|
||||
await this.dispatchEvent("load");
|
||||
}
|
||||
|
||||
async invoke(name: string, args: Array<any>): Promise<any> {
|
||||
if (!this.sandbox.isLoaded(name)) {
|
||||
const funDef = this.manifest!.functions[name];
|
||||
if (!funDef) {
|
||||
throw new Error(`Function ${name} not found in manifest`);
|
||||
}
|
||||
await this.sandbox.load(name, funDef.code!);
|
||||
}
|
||||
return await this.sandbox.invoke(name, args);
|
||||
}
|
||||
|
||||
async dispatchEvent(name: string, data?: any): Promise<any[]> {
|
||||
if (!this.manifest!.hooks?.events) {
|
||||
return [];
|
||||
}
|
||||
let functionsToSpawn = this.manifest!.hooks.events[name];
|
||||
if (functionsToSpawn) {
|
||||
return await Promise.all(
|
||||
functionsToSpawn.map((functionToSpawn: string) =>
|
||||
this.invoke(functionToSpawn, [data])
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.sandbox.stop();
|
||||
}
|
||||
}
|
||||
|
||||
export type SystemJSON<HookT> = { [key: string]: Manifest<HookT> };
|
||||
|
||||
export type SystemEvents<HookT> = {
|
||||
plugUpdated: (name: string, plug: Plug<HookT>) => void;
|
||||
plugRemoved: (name: string) => void;
|
||||
};
|
||||
|
||||
export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
||||
protected plugs = new Map<string, Plug<HookT>>();
|
||||
registeredSyscalls: SysCallMapping = {};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
registerSyscalls(...registrationObjects: SysCallMapping[]) {
|
||||
for (const registrationObject of registrationObjects) {
|
||||
for (let p in registrationObject) {
|
||||
this.registeredSyscalls[p] = registrationObject[p];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async syscall(name: string, args: Array<any>): Promise<any> {
|
||||
const callback = this.registeredSyscalls[name];
|
||||
if (!name) {
|
||||
throw Error(`Unregistered syscall ${name}`);
|
||||
}
|
||||
if (!callback) {
|
||||
throw Error(`Registered but not implemented syscall ${name}`);
|
||||
}
|
||||
return Promise.resolve(callback(...args));
|
||||
}
|
||||
|
||||
async load(
|
||||
name: string,
|
||||
manifest: Manifest<HookT>,
|
||||
sandbox: Sandbox
|
||||
): Promise<Plug<HookT>> {
|
||||
if (this.plugs.has(name)) {
|
||||
await this.unload(name);
|
||||
}
|
||||
const plug = new Plug(this, name, sandbox);
|
||||
await plug.load(manifest);
|
||||
this.plugs.set(name, plug);
|
||||
return plug;
|
||||
}
|
||||
|
||||
async unload(name: string) {
|
||||
const plug = this.plugs.get(name);
|
||||
if (!plug) {
|
||||
throw Error(`Plug ${name} not found`);
|
||||
}
|
||||
await plug.stop();
|
||||
this.plugs.delete(name);
|
||||
}
|
||||
|
||||
async dispatchEvent(name: string, data?: any): Promise<any[]> {
|
||||
let promises = [];
|
||||
for (let plug of this.plugs.values()) {
|
||||
for (let result of await plug.dispatchEvent(name, data)) {
|
||||
promises.push(result);
|
||||
}
|
||||
}
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
get loadedPlugs(): Map<string, Plug<HookT>> {
|
||||
return this.plugs;
|
||||
}
|
||||
|
||||
toJSON(): SystemJSON<HookT> {
|
||||
let plugJSON: { [key: string]: Manifest<HookT> } = {};
|
||||
for (let [name, plug] of this.plugs) {
|
||||
if (!plug.manifest) {
|
||||
continue;
|
||||
}
|
||||
plugJSON[name] = plug.manifest;
|
||||
}
|
||||
return plugJSON;
|
||||
}
|
||||
|
||||
async replaceAllFromJSON(
|
||||
json: SystemJSON<HookT>,
|
||||
sandboxFactory: () => Sandbox
|
||||
) {
|
||||
await this.unloadAll();
|
||||
for (let [name, manifest] of Object.entries(json)) {
|
||||
console.log("Loading plug", name);
|
||||
await this.load(name, manifest, sandboxFactory());
|
||||
}
|
||||
}
|
||||
|
||||
async unloadAll(): Promise<void[]> {
|
||||
return Promise.all(
|
||||
Array.from(this.plugs.keys()).map(this.unload.bind(this))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { System } from "./system";
|
||||
import {
|
||||
ControllerMessage,
|
||||
WorkerLike,
|
||||
WorkerMessage,
|
||||
} from "./environment/worker";
|
||||
|
||||
export class Sandbox {
|
||||
protected worker: WorkerLike;
|
||||
protected reqId = 0;
|
||||
protected outstandingInits = new Map<string, () => void>();
|
||||
protected outstandingInvocations = new Map<
|
||||
number,
|
||||
{ resolve: (result: any) => void; reject: (e: any) => void }
|
||||
>();
|
||||
protected loadedFunctions = new Set<string>();
|
||||
protected system: System<any>;
|
||||
|
||||
constructor(system: System<any>, worker: WorkerLike) {
|
||||
worker.onMessage = this.onMessage.bind(this);
|
||||
this.worker = worker;
|
||||
this.system = system;
|
||||
}
|
||||
|
||||
isLoaded(name: string) {
|
||||
return this.loadedFunctions.has(name);
|
||||
}
|
||||
|
||||
async load(name: string, code: string): Promise<void> {
|
||||
await this.worker.ready;
|
||||
this.worker.postMessage({
|
||||
type: "load",
|
||||
name: name,
|
||||
code: code,
|
||||
} as WorkerMessage);
|
||||
return new Promise((resolve) => {
|
||||
this.loadedFunctions.add(name);
|
||||
this.outstandingInits.set(name, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
async onMessage(data: ControllerMessage) {
|
||||
switch (data.type) {
|
||||
case "inited":
|
||||
let initCb = this.outstandingInits.get(data.name!);
|
||||
initCb && initCb();
|
||||
this.outstandingInits.delete(data.name!);
|
||||
break;
|
||||
case "syscall":
|
||||
try {
|
||||
let result = await this.system.syscall(data.name!, data.args!);
|
||||
|
||||
this.worker.postMessage({
|
||||
type: "syscall-response",
|
||||
id: data.id,
|
||||
result: result,
|
||||
} as WorkerMessage);
|
||||
} catch (e: any) {
|
||||
this.worker.postMessage({
|
||||
type: "syscall-response",
|
||||
id: data.id,
|
||||
error: e.message,
|
||||
} as WorkerMessage);
|
||||
}
|
||||
break;
|
||||
case "result":
|
||||
let resultCbs = this.outstandingInvocations.get(data.id!);
|
||||
this.outstandingInvocations.delete(data.id!);
|
||||
if (data.error) {
|
||||
resultCbs && resultCbs.reject(new Error(data.error));
|
||||
} else {
|
||||
resultCbs && resultCbs.resolve(data.result);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown message type", data);
|
||||
}
|
||||
}
|
||||
|
||||
async invoke(name: string, args: any[]): Promise<any> {
|
||||
this.reqId++;
|
||||
this.worker.postMessage({
|
||||
type: "invoke",
|
||||
id: this.reqId,
|
||||
name,
|
||||
args,
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
this.outstandingInvocations.set(this.reqId, { resolve, reject });
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.worker.terminate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Feature, Manifest, RuntimeEnvironment } from "./types";
|
||||
import { EventEmitter } from "../common/event";
|
||||
import { Sandbox } from "./sandbox";
|
||||
import { Plug } from "./plug";
|
||||
|
||||
interface SysCallMapping {
|
||||
[key: string]: (...args: any) => Promise<any> | any;
|
||||
}
|
||||
|
||||
export type SystemJSON<HookT> = { [key: string]: Manifest<HookT> };
|
||||
export type SystemEvents<HookT> = {
|
||||
plugLoaded: (name: string, plug: Plug<HookT>) => void;
|
||||
plugUnloaded: (name: string, plug: Plug<HookT>) => void;
|
||||
};
|
||||
|
||||
export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
||||
protected plugs = new Map<string, Plug<HookT>>();
|
||||
registeredSyscalls: SysCallMapping = {};
|
||||
protected enabledFeatures = new Set<Feature<HookT>>();
|
||||
|
||||
readonly runtimeEnv: RuntimeEnvironment;
|
||||
|
||||
constructor(env: RuntimeEnvironment) {
|
||||
super();
|
||||
this.runtimeEnv = env;
|
||||
}
|
||||
|
||||
addFeature(feature: Feature<HookT>) {
|
||||
this.enabledFeatures.add(feature);
|
||||
feature.apply(this);
|
||||
}
|
||||
|
||||
registerSyscalls(...registrationObjects: SysCallMapping[]) {
|
||||
for (const registrationObject of registrationObjects) {
|
||||
for (let p in registrationObject) {
|
||||
this.registeredSyscalls[p] = registrationObject[p];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async syscall(name: string, args: Array<any>): Promise<any> {
|
||||
const callback = this.registeredSyscalls[name];
|
||||
if (!name) {
|
||||
throw Error(`Unregistered syscall ${name}`);
|
||||
}
|
||||
if (!callback) {
|
||||
throw Error(`Registered but not implemented syscall ${name}`);
|
||||
}
|
||||
return Promise.resolve(callback(...args));
|
||||
}
|
||||
|
||||
async load(
|
||||
name: string,
|
||||
manifest: Manifest<HookT>,
|
||||
sandbox: Sandbox
|
||||
): Promise<Plug<HookT>> {
|
||||
if (this.plugs.has(name)) {
|
||||
await this.unload(name);
|
||||
}
|
||||
// Validate
|
||||
let errors: string[] = [];
|
||||
for (const feature of this.enabledFeatures) {
|
||||
errors = [...errors, ...feature.validateManifest(manifest)];
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Invalid manifest: ${errors.join(", ")}`);
|
||||
}
|
||||
// Ok, let's load this thing!
|
||||
const plug = new Plug(this, name, sandbox);
|
||||
await plug.load(manifest);
|
||||
this.plugs.set(name, plug);
|
||||
this.emit("plugLoaded", name, plug);
|
||||
return plug;
|
||||
}
|
||||
|
||||
async unload(name: string) {
|
||||
const plug = this.plugs.get(name);
|
||||
if (!plug) {
|
||||
throw Error(`Plug ${name} not found`);
|
||||
}
|
||||
await plug.stop();
|
||||
this.emit("plugUnloaded", name, plug);
|
||||
this.plugs.delete(name);
|
||||
}
|
||||
|
||||
async dispatchEvent(name: string, data?: any): Promise<any[]> {
|
||||
let promises = [];
|
||||
for (let plug of this.plugs.values()) {
|
||||
for (let result of await plug.dispatchEvent(name, data)) {
|
||||
promises.push(result);
|
||||
}
|
||||
}
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
get loadedPlugs(): Map<string, Plug<HookT>> {
|
||||
return this.plugs;
|
||||
}
|
||||
|
||||
toJSON(): SystemJSON<HookT> {
|
||||
let plugJSON: { [key: string]: Manifest<HookT> } = {};
|
||||
for (let [name, plug] of this.plugs) {
|
||||
if (!plug.manifest) {
|
||||
continue;
|
||||
}
|
||||
plugJSON[name] = plug.manifest;
|
||||
}
|
||||
return plugJSON;
|
||||
}
|
||||
|
||||
async replaceAllFromJSON(
|
||||
json: SystemJSON<HookT>,
|
||||
sandboxFactory: () => Sandbox
|
||||
) {
|
||||
await this.unloadAll();
|
||||
for (let [name, manifest] of Object.entries(json)) {
|
||||
console.log("Loading plug", name);
|
||||
await this.load(name, manifest, sandboxFactory());
|
||||
}
|
||||
}
|
||||
|
||||
async unloadAll(): Promise<void[]> {
|
||||
return Promise.all(
|
||||
Array.from(this.plugs.keys()).map(this.unload.bind(this))
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-44
@@ -1,25 +1,4 @@
|
||||
export type WorkerMessageType = "load" | "invoke" | "syscall-response";
|
||||
|
||||
export type WorkerMessage = {
|
||||
type: WorkerMessageType;
|
||||
id?: number;
|
||||
name?: string;
|
||||
code?: string;
|
||||
args?: any[];
|
||||
result?: any;
|
||||
error?: any;
|
||||
};
|
||||
|
||||
export type ControllerMessageType = "inited" | "result" | "syscall";
|
||||
|
||||
export type ControllerMessage = {
|
||||
type: ControllerMessageType;
|
||||
id?: number;
|
||||
name?: string;
|
||||
args?: any[];
|
||||
error?: string;
|
||||
result?: any;
|
||||
};
|
||||
import { System } from "./system";
|
||||
|
||||
export interface Manifest<HookT> {
|
||||
hooks: HookT & EventHook;
|
||||
@@ -31,33 +10,17 @@ export interface Manifest<HookT> {
|
||||
export interface FunctionDef {
|
||||
path?: string;
|
||||
code?: string;
|
||||
env?: RuntimeEnvironment;
|
||||
}
|
||||
|
||||
export type RuntimeEnvironment = "client" | "server";
|
||||
|
||||
export type EventHook = {
|
||||
events?: { [key: string]: string[] };
|
||||
};
|
||||
|
||||
export type EndpointHook = {
|
||||
endpoints?: EndPointDef[];
|
||||
};
|
||||
export type EndPointDef = {
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS";
|
||||
path: string;
|
||||
handler: string; // function name
|
||||
};
|
||||
export interface Feature<HookT> {
|
||||
validateManifest(manifest: Manifest<HookT>): string[];
|
||||
|
||||
export type CronHook = {
|
||||
crons?: CronDef[];
|
||||
};
|
||||
|
||||
export type CronDef = {
|
||||
cron: string;
|
||||
handler: string; // function name
|
||||
};
|
||||
|
||||
export interface WorkerLike {
|
||||
ready: Promise<void>;
|
||||
onMessage?: (message: any) => Promise<void>;
|
||||
postMessage(message: any): void;
|
||||
terminate(): void;
|
||||
apply(system: System<HookT>): void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user