Massive cleanup and plugbox cleanup

This commit is contained in:
Zef Hemel
2022-03-23 15:41:12 +01:00
parent a916088215
commit 09d07d587f
34 changed files with 695 additions and 525 deletions
-11
View File
@@ -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();
}
-85
View File
@@ -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;
+30
View File
@@ -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}`);
});
+120
View File
@@ -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;
}
}
+70
View File
@@ -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;
}
}
+73
View File
@@ -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();
}
}
+3 -5
View File
@@ -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
}
+4 -4
View File
@@ -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;
-248
View File
@@ -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))
);
}
}
+96
View File
@@ -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();
}
}
+127
View File
@@ -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
View File
@@ -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;
}