Massive cleanup and plugbox cleanup
This commit is contained in:
parent
a916088215
commit
09d07d587f
@ -1,5 +1,6 @@
|
|||||||
import * as plugbox from "../plugbox/types";
|
import * as plugbox from "../plugbox/types";
|
||||||
import { EndpointHook } from "../plugbox/types";
|
import { EndpointHook } from "../plugbox/feature/endpoint";
|
||||||
|
import { CronHook } from "../plugbox/feature/node_cron";
|
||||||
|
|
||||||
export type CommandDef = {
|
export type CommandDef = {
|
||||||
// Function name to invoke
|
// Function name to invoke
|
||||||
@ -18,6 +19,7 @@ export type SilverBulletHooks = {
|
|||||||
commands?: {
|
commands?: {
|
||||||
[key: string]: CommandDef;
|
[key: string]: CommandDef;
|
||||||
};
|
};
|
||||||
} & plugbox.EndpointHook;
|
} & EndpointHook &
|
||||||
|
CronHook;
|
||||||
|
|
||||||
export type Manifest = plugbox.Manifest<SilverBulletHooks>;
|
export type Manifest = plugbox.Manifest<SilverBulletHooks>;
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"plugbox-bundle": "./dist/bundler/plugbox-bundle.js"
|
"plugbox-bundle": "./dist/bundler/plugbox-bundle.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "parcel watch",
|
"watch": "rm -rf .parcel-cache && parcel watch",
|
||||||
"build": "parcel build",
|
"build": "parcel build",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"plugs": "node dist/bundler/plugbox-bundle.js plugs/core/core.plug.json plugs/dist/core.plug.json",
|
"plugs": "node dist/bundler/plugbox-bundle.js plugs/core/core.plug.json plugs/dist/core.plug.json",
|
||||||
@ -40,7 +40,7 @@
|
|||||||
"test": {
|
"test": {
|
||||||
"source": [
|
"source": [
|
||||||
"plugbox/runtime.test.ts",
|
"plugbox/runtime.test.ts",
|
||||||
"plugbox/endpoint.test.ts",
|
"plugbox/feature/endpoint.test.ts",
|
||||||
"server/api.test.ts"
|
"server/api.test.ts"
|
||||||
],
|
],
|
||||||
"outputFormat": "commonjs",
|
"outputFormat": "commonjs",
|
||||||
|
@ -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 { safeRun } from "../util";
|
||||||
import { Sandbox, System } from "./runtime";
|
|
||||||
import { safeRun } from "./util";
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import sandboxHtml from "bundle-text:./iframe_sandbox.html";
|
import sandboxHtml from "bundle-text:./iframe_sandbox.html";
|
||||||
|
import { Sandbox } from "../sandbox";
|
||||||
|
import { System } from "../system";
|
||||||
|
import { WorkerLike } from "./worker";
|
||||||
|
|
||||||
class IFrameWrapper implements WorkerLike {
|
class IFrameWrapper implements WorkerLike {
|
||||||
private iframe: HTMLIFrameElement;
|
private iframe: HTMLIFrameElement;
|
@ -1,12 +1,11 @@
|
|||||||
import { ControllerMessage, WorkerLike, WorkerMessage } from "./types";
|
|
||||||
import { System, Sandbox } from "./runtime";
|
|
||||||
|
|
||||||
import { Worker } from "worker_threads";
|
import { Worker } from "worker_threads";
|
||||||
import * as fs from "fs";
|
import { safeRun } from "../util";
|
||||||
import { safeRun } from "./util";
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import workerCode from "bundle-text:./node_worker.ts";
|
import workerCode from "bundle-text:./node_worker.ts";
|
||||||
|
import { Sandbox } from "../sandbox";
|
||||||
|
import { System } from "../system";
|
||||||
|
import { WorkerLike } from "./worker";
|
||||||
|
|
||||||
class NodeWorkerWrapper implements WorkerLike {
|
class NodeWorkerWrapper implements WorkerLike {
|
||||||
onMessage?: (message: any) => Promise<void>;
|
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 loadedFunctions = new Map<string, Function>();
|
||||||
let pendingRequests = new Map<
|
let pendingRequests = new Map<
|
@ -1,6 +1,7 @@
|
|||||||
import { ControllerMessage, WorkerLike, WorkerMessage } from "./types";
|
import { safeRun } from "../util";
|
||||||
import { Sandbox, System } from "./runtime";
|
import { Sandbox } from "../sandbox";
|
||||||
import { safeRun } from "./util";
|
import { System } from "../system";
|
||||||
|
import { WorkerLike } from "./worker";
|
||||||
|
|
||||||
class WebWorkerWrapper implements WorkerLike {
|
class WebWorkerWrapper implements WorkerLike {
|
||||||
private worker: Worker;
|
private worker: Worker;
|
30
plugbox/environment/worker.ts
Normal file
30
plugbox/environment/worker.ts
Normal 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 { createSandbox } from "../environment/node_sandbox";
|
||||||
import { System } from "./runtime";
|
import { expect, test } from "@jest/globals";
|
||||||
import { test, expect } from "@jest/globals";
|
import { Manifest } from "../types";
|
||||||
import { EndPointDef, EndpointHook, Manifest } from "./types";
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import { exposeSystem } from "./endpoints";
|
import { EndpointFeature, EndpointHook } from "./endpoint";
|
||||||
|
import { System } from "../system";
|
||||||
|
|
||||||
test("Run a plugbox endpoint server", async () => {
|
test("Run a plugbox endpoint server", async () => {
|
||||||
let system = new System<EndpointHook>();
|
let system = new System<EndpointHook>("server");
|
||||||
let plug = await system.load(
|
let plug = await system.load(
|
||||||
"test",
|
"test",
|
||||||
{
|
{
|
||||||
@ -32,7 +32,9 @@ test("Run a plugbox endpoint server", async () => {
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 3123;
|
const port = 3123;
|
||||||
app.use(exposeSystem(system));
|
|
||||||
|
system.addFeature(new EndpointFeature(app));
|
||||||
|
|
||||||
let server = app.listen(port, () => {
|
let server = app.listen(port, () => {
|
||||||
console.log(`Listening on port ${port}`);
|
console.log(`Listening on port ${port}`);
|
||||||
});
|
});
|
120
plugbox/feature/endpoint.ts
Normal file
120
plugbox/feature/endpoint.ts
Normal 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
plugbox/feature/node_cron.ts
Normal file
70
plugbox/feature/node_cron.ts
Normal 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
plugbox/plug.ts
Normal file
73
plugbox/plug.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import fs, { stat, watch } from "fs/promises";
|
import fs, { watch } from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { createSandbox } from "./node_sandbox";
|
import { createSandbox } from "./environment/node_sandbox";
|
||||||
import { System } from "./runtime";
|
|
||||||
import { safeRun } from "../server/util";
|
import { safeRun } from "../server/util";
|
||||||
|
import { System } from "./system";
|
||||||
|
|
||||||
function extractPlugName(localPath: string): string {
|
function extractPlugName(localPath: string): string {
|
||||||
const baseName = path.basename(localPath);
|
const baseName = path.basename(localPath);
|
||||||
@ -34,10 +34,8 @@ export class DiskPlugLoader<HookT> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Likely removed
|
// Likely removed
|
||||||
await this.system.unload(plugName);
|
await this.system.unload(plugName);
|
||||||
this.system.emit("plugRemoved", plugName);
|
|
||||||
}
|
}
|
||||||
const plugDef = await this.loadPlugFromFile(localPath);
|
const plugDef = await this.loadPlugFromFile(localPath);
|
||||||
this.system.emit("plugUpdated", plugName, plugDef);
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore, error handled by loadPlug
|
// ignore, error handled by loadPlug
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { createSandbox } from "./node_sandbox";
|
import { createSandbox } from "./environment/node_sandbox";
|
||||||
import { System } from "./runtime";
|
import { expect, test } from "@jest/globals";
|
||||||
import { test, expect } from "@jest/globals";
|
import { System } from "./system";
|
||||||
|
|
||||||
test("Run a Node sandbox", async () => {
|
test("Run a Node sandbox", async () => {
|
||||||
let system = new System();
|
let system = new System("server");
|
||||||
system.registerSyscalls({
|
system.registerSyscalls({
|
||||||
addNumbers: (a, b) => {
|
addNumbers: (a, b) => {
|
||||||
return 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))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
96
plugbox/sandbox.ts
Normal file
96
plugbox/sandbox.ts
Normal 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
plugbox/system.ts
Normal file
127
plugbox/system.ts
Normal 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))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,4 @@
|
|||||||
export type WorkerMessageType = "load" | "invoke" | "syscall-response";
|
import { System } from "./system";
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Manifest<HookT> {
|
export interface Manifest<HookT> {
|
||||||
hooks: HookT & EventHook;
|
hooks: HookT & EventHook;
|
||||||
@ -31,33 +10,17 @@ export interface Manifest<HookT> {
|
|||||||
export interface FunctionDef {
|
export interface FunctionDef {
|
||||||
path?: string;
|
path?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
|
env?: RuntimeEnvironment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RuntimeEnvironment = "client" | "server";
|
||||||
|
|
||||||
export type EventHook = {
|
export type EventHook = {
|
||||||
events?: { [key: string]: string[] };
|
events?: { [key: string]: string[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EndpointHook = {
|
export interface Feature<HookT> {
|
||||||
endpoints?: EndPointDef[];
|
validateManifest(manifest: Manifest<HookT>): string[];
|
||||||
};
|
|
||||||
export type EndPointDef = {
|
|
||||||
method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS";
|
|
||||||
path: string;
|
|
||||||
handler: string; // function name
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CronHook = {
|
apply(system: System<HookT>): void;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,12 @@
|
|||||||
"path": "/",
|
"path": "/",
|
||||||
"handler": "endpointTest"
|
"handler": "endpointTest"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"crons": [
|
||||||
|
{
|
||||||
|
"cron": "*/15 * * * *",
|
||||||
|
"handler": "gitSnapshot"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"functions": {
|
"functions": {
|
||||||
@ -88,7 +94,12 @@
|
|||||||
"path": "./server.ts:endpointTest"
|
"path": "./server.ts:endpointTest"
|
||||||
},
|
},
|
||||||
"welcome": {
|
"welcome": {
|
||||||
"path": "./server.ts:welcome"
|
"path": "./server.ts:welcome",
|
||||||
|
"env": "server"
|
||||||
|
},
|
||||||
|
"gitSnapshot": {
|
||||||
|
"path": "./git.ts:commit",
|
||||||
|
"env": "server"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
plugs/core/git.ts
Normal file
12
plugs/core/git.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { syscall } from "./lib/syscall";
|
||||||
|
|
||||||
|
export async function commit() {
|
||||||
|
console.log("Snapshotting the current space to git");
|
||||||
|
await syscall("shell.run", "git", ["add", "./*.md"]);
|
||||||
|
try {
|
||||||
|
await syscall("shell.run", "git", ["commit", "-a", "-m", "Snapshot"]);
|
||||||
|
} catch (e) {
|
||||||
|
// We can ignore, this happens when there's no changes to commit
|
||||||
|
}
|
||||||
|
console.log("Done!");
|
||||||
|
}
|
@ -57,10 +57,10 @@ export async function renamePage() {
|
|||||||
let text = await syscall("editor.getText");
|
let text = await syscall("editor.getText");
|
||||||
console.log("Writing new page to space");
|
console.log("Writing new page to space");
|
||||||
await syscall("space.writePage", newName, text);
|
await syscall("space.writePage", newName, text);
|
||||||
console.log("Deleting page from space");
|
|
||||||
await syscall("space.deletePage", oldName);
|
|
||||||
console.log("Navigating to new page");
|
console.log("Navigating to new page");
|
||||||
await syscall("editor.navigate", newName);
|
await syscall("editor.navigate", newName);
|
||||||
|
console.log("Deleting page from space");
|
||||||
|
await syscall("space.deletePage", oldName);
|
||||||
|
|
||||||
let pageToUpdateSet = new Set<string>();
|
let pageToUpdateSet = new Set<string>();
|
||||||
for (let pageToUpdate of pagesToUpdate) {
|
for (let pageToUpdate of pagesToUpdate) {
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { EndpointRequest, EndpointResponse } from "../../plugbox/endpoints";
|
import {
|
||||||
|
EndpointRequest,
|
||||||
|
EndpointResponse,
|
||||||
|
} from "../../plugbox/feature/endpoint";
|
||||||
|
|
||||||
export function endpointTest(req: EndpointRequest): EndpointResponse {
|
export function endpointTest(req: EndpointRequest): EndpointResponse {
|
||||||
console.log("I'm running on the server!", req);
|
console.log("I'm running on the server!", req);
|
||||||
@ -9,7 +12,5 @@ export function endpointTest(req: EndpointRequest): EndpointResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function welcome() {
|
export function welcome() {
|
||||||
for (var i = 0; i < 10; i++) {
|
console.log("Hello world!");
|
||||||
console.log("Welcome to you all!!!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { test, expect, beforeAll, afterAll, describe } from "@jest/globals";
|
import { afterAll, beforeAll, describe, expect, test } from "@jest/globals";
|
||||||
|
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import { io as Client } from "socket.io-client";
|
import { io as Client } from "socket.io-client";
|
||||||
@ -7,7 +7,7 @@ import { SocketServer } from "./api_server";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { SilverBulletHooks } from "../common/manifest";
|
import { SilverBulletHooks } from "../common/manifest";
|
||||||
import { System } from "../plugbox/runtime";
|
import { System } from "../plugbox/system";
|
||||||
|
|
||||||
describe("Server test", () => {
|
describe("Server test", () => {
|
||||||
let io: Server,
|
let io: Server,
|
||||||
@ -43,7 +43,7 @@ describe("Server test", () => {
|
|||||||
socketServer = new SocketServer(
|
socketServer = new SocketServer(
|
||||||
tmpDir,
|
tmpDir,
|
||||||
io,
|
io,
|
||||||
new System<SilverBulletHooks>()
|
new System<SilverBulletHooks>("server")
|
||||||
);
|
);
|
||||||
clientSocket.on("connect", done);
|
clientSocket.on("connect", done);
|
||||||
await socketServer.init();
|
await socketServer.init();
|
||||||
|
@ -3,17 +3,20 @@ import { Page } from "./types";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { IndexApi } from "./index_api";
|
import { IndexApi } from "./index_api";
|
||||||
import { PageApi } from "./page_api";
|
import { PageApi } from "./page_api";
|
||||||
import { System } from "../plugbox/runtime";
|
|
||||||
import { SilverBulletHooks } from "../common/manifest";
|
import { SilverBulletHooks } from "../common/manifest";
|
||||||
import pageIndexSyscalls from "./syscalls/page_index";
|
import pageIndexSyscalls from "./syscalls/page_index";
|
||||||
|
import { safeRun } from "./util";
|
||||||
|
import { System } from "../plugbox/system";
|
||||||
|
|
||||||
export class ClientConnection {
|
export class ClientConnection {
|
||||||
openPages = new Set<string>();
|
openPages = new Set<string>();
|
||||||
|
|
||||||
constructor(readonly sock: Socket) {}
|
constructor(readonly sock: Socket) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiProvider {
|
export interface ApiProvider {
|
||||||
init(): Promise<void>;
|
init(): Promise<void>;
|
||||||
|
|
||||||
api(): Object;
|
api(): Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,13 +65,19 @@ export class SocketServer {
|
|||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
console.log("Disconnected", socket.id);
|
console.log("Disconnected", socket.id);
|
||||||
clientConn.openPages.forEach(disconnectPageSocket);
|
clientConn.openPages.forEach((pageName) => {
|
||||||
|
safeRun(async () => {
|
||||||
|
await disconnectPageSocket(pageName);
|
||||||
|
});
|
||||||
|
});
|
||||||
this.connectedSockets.delete(socket);
|
this.connectedSockets.delete(socket);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("page.closePage", (pageName: string) => {
|
socket.on("page.closePage", (pageName: string) => {
|
||||||
console.log("Client closed page", pageName);
|
console.log("Client closed page", pageName);
|
||||||
disconnectPageSocket(pageName);
|
safeRun(async () => {
|
||||||
|
await disconnectPageSocket(pageName);
|
||||||
|
});
|
||||||
clientConn.openPages.delete(pageName);
|
clientConn.openPages.delete(pageName);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -87,12 +96,12 @@ export class SocketServer {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const disconnectPageSocket = (pageName: string) => {
|
const disconnectPageSocket = async (pageName: string) => {
|
||||||
let page = this.openPages.get(pageName);
|
let page = this.openPages.get(pageName);
|
||||||
if (page) {
|
if (page) {
|
||||||
for (let client of page.clientStates) {
|
for (let client of page.clientStates) {
|
||||||
if (client.socket === socket) {
|
if (client.socket === socket) {
|
||||||
(this.apis.get("page")! as PageApi).disconnectClient(
|
await (this.apis.get("page")! as PageApi).disconnectClient(
|
||||||
client,
|
client,
|
||||||
page
|
page
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { readdir, readFile, stat, unlink, writeFile } from "fs/promises";
|
import { mkdir, readdir, readFile, stat, unlink, writeFile } from "fs/promises";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { PageMeta } from "./types";
|
import { PageMeta } from "./types";
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ export class DiskStorage {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error("Error while writing page", pageName, e);
|
// console.error("Error while reading page", pageName, e);
|
||||||
throw Error(`Could not read page ${pageName}`);
|
throw Error(`Could not read page ${pageName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,9 +56,13 @@ export class DiskStorage {
|
|||||||
async writePage(pageName: string, text: string): Promise<PageMeta> {
|
async writePage(pageName: string, text: string): Promise<PageMeta> {
|
||||||
let localPath = path.join(this.rootPath, pageName + ".md");
|
let localPath = path.join(this.rootPath, pageName + ".md");
|
||||||
try {
|
try {
|
||||||
|
// Ensure parent folder exists
|
||||||
|
await mkdir(path.dirname(localPath), { recursive: true });
|
||||||
|
|
||||||
|
// Actually write the file
|
||||||
await writeFile(localPath, text);
|
await writeFile(localPath, text);
|
||||||
|
|
||||||
// console.log(`Wrote to ${localPath}`);
|
// Fetch new metadata
|
||||||
const s = await stat(localPath);
|
const s = await stat(localPath);
|
||||||
return {
|
return {
|
||||||
name: pageName,
|
name: pageName,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { Express } from "express";
|
import { Express } from "express";
|
||||||
import { System } from "../plugbox/runtime";
|
|
||||||
import { SilverBulletHooks } from "../common/manifest";
|
import { SilverBulletHooks } from "../common/manifest";
|
||||||
import { exposeSystem } from "../plugbox/endpoints";
|
import { EndpointFeature } from "../plugbox/feature/endpoint";
|
||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
|
import { System } from "../plugbox/system";
|
||||||
|
|
||||||
export class ExpressServer {
|
export class ExpressServer {
|
||||||
app: Express;
|
app: Express;
|
||||||
@ -19,7 +19,7 @@ export class ExpressServer {
|
|||||||
this.rootPath = rootPath;
|
this.rootPath = rootPath;
|
||||||
this.system = system;
|
this.system = system;
|
||||||
|
|
||||||
app.use(exposeSystem(this.system));
|
system.addFeature(new EndpointFeature(app));
|
||||||
|
|
||||||
// Fallback, serve index.html
|
// Fallback, serve index.html
|
||||||
let cachedIndex: string | undefined = undefined;
|
let cachedIndex: string | undefined = undefined;
|
||||||
|
@ -9,8 +9,8 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { stat } from "fs/promises";
|
import { stat } from "fs/promises";
|
||||||
import { Cursor, cursorEffect } from "../webapp/cursorEffect";
|
import { Cursor, cursorEffect } from "../webapp/cursorEffect";
|
||||||
import { System } from "../plugbox/runtime";
|
|
||||||
import { SilverBulletHooks } from "../common/manifest";
|
import { SilverBulletHooks } from "../common/manifest";
|
||||||
|
import { System } from "../plugbox/system";
|
||||||
|
|
||||||
export class PageApi implements ApiProvider {
|
export class PageApi implements ApiProvider {
|
||||||
openPages: Map<string, Page>;
|
openPages: Map<string, Page>;
|
||||||
@ -34,17 +34,18 @@ export class PageApi implements ApiProvider {
|
|||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
this.fileWatcher();
|
this.fileWatcher();
|
||||||
|
// TODO: Move this elsewhere, this doesn't belong here
|
||||||
this.system.on({
|
this.system.on({
|
||||||
plugUpdated: (plugName, plugDef) => {
|
plugLoaded: (plugName, plugDef) => {
|
||||||
console.log("Plug updated on disk, broadcasting to all clients");
|
console.log("Plug updated on disk, broadcasting to all clients");
|
||||||
this.connectedSockets.forEach((socket) => {
|
this.connectedSockets.forEach((socket) => {
|
||||||
socket.emit("plugUpdated", plugName, plugDef);
|
socket.emit("plugLoaded", plugName, plugDef.manifest);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
plugRemoved: (plugName) => {
|
plugUnloaded: (plugName) => {
|
||||||
console.log("Plug removed on disk, broadcasting to all clients");
|
console.log("Plug removed on disk, broadcasting to all clients");
|
||||||
this.connectedSockets.forEach((socket) => {
|
this.connectedSockets.forEach((socket) => {
|
||||||
socket.emit("plugRemoved", plugName);
|
socket.emit("plugUnloaded", plugName);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -53,27 +54,25 @@ export class PageApi implements ApiProvider {
|
|||||||
broadcastCursors(page: Page) {
|
broadcastCursors(page: Page) {
|
||||||
page.clientStates.forEach((client) => {
|
page.clientStates.forEach((client) => {
|
||||||
client.socket.emit(
|
client.socket.emit(
|
||||||
"cursorSnapshot",
|
"cursorSnapshot",
|
||||||
page.name,
|
page.name,
|
||||||
Object.fromEntries(page.cursors.entries())
|
Object.fromEntries(page.cursors.entries())
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
flushPageToDisk(name: string, page: Page) {
|
async flushPageToDisk(name: string, page: Page) {
|
||||||
safeRun(async () => {
|
let meta = await this.pageStore.writePage(name, page.text.sliceString(0));
|
||||||
let meta = await this.pageStore.writePage(name, page.text.sliceString(0));
|
console.log(`Wrote page ${name} to disk`);
|
||||||
console.log(`Wrote page ${name} to disk`);
|
page.meta = meta;
|
||||||
page.meta = meta;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectClient(client: ClientPageState, page: Page) {
|
async disconnectClient(client: ClientPageState, page: Page) {
|
||||||
console.log("Disconnecting client");
|
console.log("Disconnecting client");
|
||||||
page.clientStates.delete(client);
|
page.clientStates.delete(client);
|
||||||
if (page.clientStates.size === 0) {
|
if (page.clientStates.size === 0) {
|
||||||
console.log("No more clients for", page.name, "flushing");
|
console.log("No more clients for", page.name, "flushing");
|
||||||
this.flushPageToDisk(page.name, page);
|
await this.flushPageToDisk(page.name, page);
|
||||||
this.openPages.delete(page.name);
|
this.openPages.delete(page.name);
|
||||||
} else {
|
} else {
|
||||||
page.cursors.delete(client.socket.id);
|
page.cursors.delete(client.socket.id);
|
||||||
@ -214,16 +213,22 @@ export class PageApi implements ApiProvider {
|
|||||||
// Throttle
|
// Throttle
|
||||||
if (!page.saveTimer) {
|
if (!page.saveTimer) {
|
||||||
page.saveTimer = setTimeout(() => {
|
page.saveTimer = setTimeout(() => {
|
||||||
if (page) {
|
safeRun(async () => {
|
||||||
console.log("Indexing", pageName);
|
if (page) {
|
||||||
|
console.log(
|
||||||
|
"Persisting",
|
||||||
|
pageName,
|
||||||
|
" to disk and indexing."
|
||||||
|
);
|
||||||
|
await this.flushPageToDisk(pageName, page);
|
||||||
|
|
||||||
this.system.dispatchEvent("page:index", {
|
await this.system.dispatchEvent("page:index", {
|
||||||
name: pageName,
|
name: pageName,
|
||||||
text: page.text.sliceString(0),
|
text: page.text.sliceString(0),
|
||||||
});
|
});
|
||||||
this.flushPageToDisk(pageName, page);
|
page.saveTimer = undefined;
|
||||||
page.saveTimer = undefined;
|
}
|
||||||
}
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,12 @@ import { Server } from "socket.io";
|
|||||||
import { SocketServer } from "./api_server";
|
import { SocketServer } from "./api_server";
|
||||||
import yargs from "yargs";
|
import yargs from "yargs";
|
||||||
import { hideBin } from "yargs/helpers";
|
import { hideBin } from "yargs/helpers";
|
||||||
import { System } from "../plugbox/runtime";
|
|
||||||
import { SilverBulletHooks } from "../common/manifest";
|
import { SilverBulletHooks } from "../common/manifest";
|
||||||
import { ExpressServer } from "./express_server";
|
import { ExpressServer } from "./express_server";
|
||||||
import { DiskPlugLoader } from "../plugbox/plug_loader";
|
import { DiskPlugLoader } from "../plugbox/plug_loader";
|
||||||
|
import { NodeCronFeature } from "../plugbox/feature/node_cron";
|
||||||
|
import shellSyscalls from "./syscalls/shell";
|
||||||
|
import { System } from "../plugbox/system";
|
||||||
|
|
||||||
let args = yargs(hideBin(process.argv))
|
let args = yargs(hideBin(process.argv))
|
||||||
.option("debug", {
|
.option("debug", {
|
||||||
@ -23,7 +25,7 @@ const pagesPath = args._[0] as string;
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const system = new System<SilverBulletHooks>();
|
const system = new System<SilverBulletHooks>("server");
|
||||||
|
|
||||||
const io = new Server(server, {
|
const io = new Server(server, {
|
||||||
cors: {
|
cors: {
|
||||||
@ -52,6 +54,8 @@ expressServer
|
|||||||
);
|
);
|
||||||
await plugLoader.loadPlugs();
|
await plugLoader.loadPlugs();
|
||||||
plugLoader.watcher();
|
plugLoader.watcher();
|
||||||
|
system.registerSyscalls(shellSyscalls(pagesPath));
|
||||||
|
system.addFeature(new NodeCronFeature());
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
console.log(`Server listening on port ${port}`);
|
console.log(`Server listening on port ${port}`);
|
||||||
});
|
});
|
||||||
|
@ -21,8 +21,7 @@ import {
|
|||||||
} from "@codemirror/view";
|
} from "@codemirror/view";
|
||||||
import React, { useEffect, useReducer } from "react";
|
import React, { useEffect, useReducer } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { Plug, System } from "../plugbox/runtime";
|
import { createSandbox as createIFrameSandbox } from "../plugbox/environment/iframe_sandbox";
|
||||||
import { createSandbox as createIFrameSandbox } from "../plugbox/iframe_sandbox";
|
|
||||||
import { AppEvent, AppEventDispatcher, ClickEvent } from "./app_event";
|
import { AppEvent, AppEventDispatcher, ClickEvent } from "./app_event";
|
||||||
import { CollabDocument, collabExtension } from "./collab";
|
import { CollabDocument, collabExtension } from "./collab";
|
||||||
import * as commands from "./commands";
|
import * as commands from "./commands";
|
||||||
@ -38,7 +37,6 @@ import reducer from "./reducer";
|
|||||||
import { smartQuoteKeymap } from "./smart_quotes";
|
import { smartQuoteKeymap } from "./smart_quotes";
|
||||||
import { Space } from "./space";
|
import { Space } from "./space";
|
||||||
import customMarkdownStyle from "./style";
|
import customMarkdownStyle from "./style";
|
||||||
import dbSyscalls from "./syscalls/db.localstorage";
|
|
||||||
import editorSyscalls from "./syscalls/editor.browser";
|
import editorSyscalls from "./syscalls/editor.browser";
|
||||||
import indexerSyscalls from "./syscalls/indexer.native";
|
import indexerSyscalls from "./syscalls/indexer.native";
|
||||||
import spaceSyscalls from "./syscalls/space.native";
|
import spaceSyscalls from "./syscalls/space.native";
|
||||||
@ -51,6 +49,7 @@ import {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
import { SilverBulletHooks } from "../common/manifest";
|
import { SilverBulletHooks } from "../common/manifest";
|
||||||
import { safeRun } from "./util";
|
import { safeRun } from "./util";
|
||||||
|
import { System } from "../plugbox/system";
|
||||||
|
|
||||||
class PageState {
|
class PageState {
|
||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
@ -63,19 +62,17 @@ class PageState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Editor implements AppEventDispatcher {
|
export class Editor implements AppEventDispatcher {
|
||||||
private system = new System<SilverBulletHooks>();
|
private system = new System<SilverBulletHooks>("client");
|
||||||
|
openPages = new Map<string, PageState>();
|
||||||
|
editorCommands = new Map<string, AppCommand>();
|
||||||
editorView?: EditorView;
|
editorView?: EditorView;
|
||||||
viewState: AppViewState;
|
viewState: AppViewState;
|
||||||
viewDispatch: React.Dispatch<Action>;
|
viewDispatch: React.Dispatch<Action>;
|
||||||
openPages: Map<string, PageState>;
|
|
||||||
space: Space;
|
space: Space;
|
||||||
editorCommands: Map<string, AppCommand>;
|
|
||||||
navigationResolve?: (val: undefined) => void;
|
navigationResolve?: (val: undefined) => void;
|
||||||
pageNavigator: IPageNavigator;
|
pageNavigator: IPageNavigator;
|
||||||
|
|
||||||
constructor(space: Space, parent: Element) {
|
constructor(space: Space, parent: Element) {
|
||||||
this.editorCommands = new Map();
|
|
||||||
this.openPages = new Map();
|
|
||||||
this.space = space;
|
this.space = space;
|
||||||
this.viewState = initialViewState;
|
this.viewState = initialViewState;
|
||||||
this.viewDispatch = () => {};
|
this.viewDispatch = () => {};
|
||||||
@ -90,7 +87,6 @@ export class Editor implements AppEventDispatcher {
|
|||||||
this.pageNavigator = new PathPageNavigator();
|
this.pageNavigator = new PathPageNavigator();
|
||||||
|
|
||||||
this.system.registerSyscalls(
|
this.system.registerSyscalls(
|
||||||
dbSyscalls,
|
|
||||||
editorSyscalls(this),
|
editorSyscalls(this),
|
||||||
spaceSyscalls(this),
|
spaceSyscalls(this),
|
||||||
indexerSyscalls(this.space)
|
indexerSyscalls(this.space)
|
||||||
@ -98,7 +94,6 @@ export class Editor implements AppEventDispatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await this.loadPlugs();
|
|
||||||
this.focus();
|
this.focus();
|
||||||
|
|
||||||
this.pageNavigator.subscribe(async (pageName) => {
|
this.pageNavigator.subscribe(async (pageName) => {
|
||||||
@ -134,34 +129,26 @@ export class Editor implements AppEventDispatcher {
|
|||||||
},
|
},
|
||||||
loadSystem: (systemJSON) => {
|
loadSystem: (systemJSON) => {
|
||||||
safeRun(async () => {
|
safeRun(async () => {
|
||||||
console.log("Received SYSTEM", systemJSON);
|
|
||||||
await this.system.replaceAllFromJSON(systemJSON, () =>
|
await this.system.replaceAllFromJSON(systemJSON, () =>
|
||||||
createIFrameSandbox(this.system)
|
createIFrameSandbox(this.system)
|
||||||
);
|
);
|
||||||
console.log("Loaded plugs, now updating editor comands");
|
this.buildAllCommands();
|
||||||
this.editorCommands = new Map<string, AppCommand>();
|
|
||||||
for (let plug of this.system.loadedPlugs.values()) {
|
|
||||||
this.buildCommands(plug);
|
|
||||||
}
|
|
||||||
this.viewDispatch({
|
|
||||||
type: "update-commands",
|
|
||||||
commands: this.editorCommands,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
plugUpdated: (plugName, plug) => {
|
plugLoaded: (plugName, plug) => {
|
||||||
safeRun(async () => {
|
safeRun(async () => {
|
||||||
console.log("Plug updated", plugName);
|
console.log("Plug load", plugName);
|
||||||
await this.system.load(
|
await this.system.load(
|
||||||
plugName,
|
plugName,
|
||||||
plug,
|
plug,
|
||||||
createIFrameSandbox(this.system)
|
createIFrameSandbox(this.system)
|
||||||
);
|
);
|
||||||
|
this.buildAllCommands();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
plugRemoved: (plugName) => {
|
plugUnloaded: (plugName) => {
|
||||||
safeRun(async () => {
|
safeRun(async () => {
|
||||||
console.log("Plug removed", plugName);
|
console.log("Plug unload", plugName);
|
||||||
await this.system.unload(plugName);
|
await this.system.unload(plugName);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -172,6 +159,27 @@ export class Editor implements AppEventDispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildAllCommands() {
|
||||||
|
console.log("Loaded plugs, now updating editor commands");
|
||||||
|
this.editorCommands.clear();
|
||||||
|
for (let plug of this.system.loadedPlugs.values()) {
|
||||||
|
const cmds = plug.manifest!.hooks.commands;
|
||||||
|
for (let name in cmds) {
|
||||||
|
let cmd = cmds[name];
|
||||||
|
this.editorCommands.set(name, {
|
||||||
|
command: cmd,
|
||||||
|
run: async (arg): Promise<any> => {
|
||||||
|
return await plug.invoke(cmd.invoke, [arg]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.viewDispatch({
|
||||||
|
type: "update-commands",
|
||||||
|
commands: this.editorCommands,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
flashNotification(message: string) {
|
flashNotification(message: string) {
|
||||||
let id = Math.floor(Math.random() * 1000000);
|
let id = Math.floor(Math.random() * 1000000);
|
||||||
this.viewDispatch({
|
this.viewDispatch({
|
||||||
@ -190,29 +198,6 @@ export class Editor implements AppEventDispatcher {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPlugs() {
|
|
||||||
const system = new System<SilverBulletHooks>();
|
|
||||||
system.registerSyscalls(
|
|
||||||
dbSyscalls,
|
|
||||||
editorSyscalls(this),
|
|
||||||
spaceSyscalls(this),
|
|
||||||
indexerSyscalls(this.space)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private buildCommands(plug: Plug<SilverBulletHooks>) {
|
|
||||||
const cmds = plug.manifest!.hooks.commands;
|
|
||||||
for (let name in cmds) {
|
|
||||||
let cmd = cmds[name];
|
|
||||||
this.editorCommands.set(name, {
|
|
||||||
command: cmd,
|
|
||||||
run: async (arg): Promise<any> => {
|
|
||||||
return await plug.invoke(cmd.invoke, [arg]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]> {
|
async dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]> {
|
||||||
return this.system.dispatchEvent(name, data);
|
return this.system.dispatchEvent(name, data);
|
||||||
}
|
}
|
||||||
@ -349,7 +334,6 @@ export class Editor implements AppEventDispatcher {
|
|||||||
|
|
||||||
async plugCompleter(): Promise<CompletionResult | null> {
|
async plugCompleter(): Promise<CompletionResult | null> {
|
||||||
let allCompletionResults = await this.dispatchAppEvent("editor:complete");
|
let allCompletionResults = await this.dispatchAppEvent("editor:complete");
|
||||||
console.log("Completion results", allCompletionResults);
|
|
||||||
if (allCompletionResults.length === 1) {
|
if (allCompletionResults.length === 1) {
|
||||||
return allCompletionResults[0];
|
return allCompletionResults[0];
|
||||||
} else if (allCompletionResults.length > 1) {
|
} else if (allCompletionResults.length > 1) {
|
||||||
@ -398,8 +382,8 @@ export class Editor implements AppEventDispatcher {
|
|||||||
this.editorView!.focus();
|
this.editorView!.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(name: string) {
|
async navigate(name: string) {
|
||||||
this.pageNavigator.navigate(name);
|
await this.pageNavigator.navigate(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPage(pageName: string) {
|
async loadPage(pageName: string) {
|
||||||
|
@ -2,7 +2,9 @@ import { safeRun } from "./util";
|
|||||||
|
|
||||||
export interface IPageNavigator {
|
export interface IPageNavigator {
|
||||||
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void;
|
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void;
|
||||||
navigate(page: string): void;
|
|
||||||
|
navigate(page: string): Promise<void>;
|
||||||
|
|
||||||
getCurrentPage(): string;
|
getCurrentPage(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@ import { ChangeSet, Text, Transaction } from "@codemirror/state";
|
|||||||
import { CollabDocument, CollabEvents } from "./collab";
|
import { CollabDocument, CollabEvents } from "./collab";
|
||||||
import { cursorEffect } from "./cursorEffect";
|
import { cursorEffect } from "./cursorEffect";
|
||||||
import { EventEmitter } from "../common/event";
|
import { EventEmitter } from "../common/event";
|
||||||
import { SystemJSON } from "../plugbox/runtime";
|
|
||||||
import { Manifest } from "../common/manifest";
|
import { Manifest } from "../common/manifest";
|
||||||
|
import { SystemJSON } from "../plugbox/system";
|
||||||
|
|
||||||
export type SpaceEvents = {
|
export type SpaceEvents = {
|
||||||
connect: () => void;
|
connect: () => void;
|
||||||
@ -16,8 +16,8 @@ export type SpaceEvents = {
|
|||||||
pageDeleted: (name: string) => void;
|
pageDeleted: (name: string) => void;
|
||||||
pageListUpdated: (pages: Set<PageMeta>) => void;
|
pageListUpdated: (pages: Set<PageMeta>) => void;
|
||||||
loadSystem: (systemJSON: SystemJSON<any>) => void;
|
loadSystem: (systemJSON: SystemJSON<any>) => void;
|
||||||
plugUpdated: (plugName: string, plug: Manifest) => void;
|
plugLoaded: (plugName: string, plug: Manifest) => void;
|
||||||
plugRemoved: (plugName: string) => void;
|
plugUnloaded: (plugName: string) => void;
|
||||||
} & CollabEvents;
|
} & CollabEvents;
|
||||||
|
|
||||||
export type KV = {
|
export type KV = {
|
||||||
@ -41,8 +41,8 @@ export class Space extends EventEmitter<SpaceEvents> {
|
|||||||
"pageChanged",
|
"pageChanged",
|
||||||
"pageDeleted",
|
"pageDeleted",
|
||||||
"loadSystem",
|
"loadSystem",
|
||||||
"plugUpdated",
|
"plugLoaded",
|
||||||
"plugRemoved",
|
"plugUnloaded",
|
||||||
].forEach((eventName) => {
|
].forEach((eventName) => {
|
||||||
socket.on(eventName, (...args) => {
|
socket.on(eventName, (...args) => {
|
||||||
this.emit(eventName as keyof SpaceEvents, ...args);
|
this.emit(eventName as keyof SpaceEvents, ...args);
|
||||||
|
Loading…
Reference in New Issue
Block a user