1
0

Tons of refactoring, moving commands and slash commands into hooks

This commit is contained in:
Zef Hemel 2022-03-29 11:21:32 +02:00
parent bf32d6d0bd
commit b89aee97d7
42 changed files with 330 additions and 224 deletions

View File

@ -1,24 +1,14 @@
import * as plugos from "../plugos/types"; import * as plugos from "../plugos/types";
import { EndpointHook } from "../plugos/feature/endpoint"; import { EndpointHookT } from "../plugos/hooks/endpoint";
import { CronHook } from "../plugos/feature/node_cron"; import { CronHookT } from "../plugos/hooks/node_cron";
import { EventHook } from "../plugos/feature/event"; import { EventHookT } from "../plugos/hooks/event";
import { CommandHookT } from "../webapp/hooks/command";
import { SlashCommandHookT } from "../webapp/hooks/slash_command";
export type CommandDef = { export type SilverBulletHooks = CommandHookT &
name: string; SlashCommandHookT &
EndpointHookT &
// Bind to keyboard shortcut CronHookT &
key?: string; EventHookT;
mac?: string;
// If to show in slash invoked menu and if so, with what label
// should match slashCommandRegexp
slashCommand?: string;
};
export type SilverBulletHooks = {
command?: CommandDef | CommandDef[];
} & EndpointHook &
CronHook &
EventHook;
export type Manifest = plugos.Manifest<SilverBulletHooks>; export type Manifest = plugos.Manifest<SilverBulletHooks>;

View File

@ -14,7 +14,7 @@
"clean": "rm -rf dist", "clean": "rm -rf dist",
"plugs": "cd plugs && ../plugos/dist/plugos/plugos-bundle.js -w --dist dist */*.plug.yaml", "plugs": "cd plugs && ../plugos/dist/plugos/plugos-bundle.js -w --dist dist */*.plug.yaml",
"server": "nodemon -w dist/server dist/server/server.js pages", "server": "nodemon -w dist/server dist/server/server.js pages",
"test": "jest dist/test" "test": "jest dist/test plugos/dist/test"
}, },
"files": [ "files": [
"dist" "dist"

View File

@ -2,22 +2,22 @@
import express from "express"; import express from "express";
import yargs from "yargs"; import yargs from "yargs";
import { hideBin } from "yargs/helpers"; import {hideBin} from "yargs/helpers";
import { DiskPlugLoader } from "../plug_loader"; import {DiskPlugLoader} from "../plug_loader";
import { CronHook, NodeCronFeature } from "../feature/node_cron"; import {CronHookT, NodeCronHook} from "../hooks/node_cron";
import shellSyscalls from "../syscall/shell.node"; import shellSyscalls from "../syscalls/shell.node";
import { System } from "../system"; import {System} from "../system";
import { EndpointFeature, EndpointHook } from "../feature/endpoint"; import {EndpointHook, EndpointHookT} from "../hooks/endpoint";
import { safeRun } from "../util"; import {safeRun} from "../util";
import knex from "knex"; import knex from "knex";
import { import {
ensureTable, ensureTable,
storeReadSyscalls, storeReadSyscalls,
storeWriteSyscalls, storeWriteSyscalls,
} from "../syscall/store.knex_node"; } from "../syscalls/store.knex_node";
import { fetchSyscalls } from "../syscall/fetch.node"; import {fetchSyscalls} from "../syscalls/fetch.node";
import { EventFeature, EventHook } from "../feature/event"; import {EventHook, EventHookT} from "../hooks/event";
import { eventSyscalls } from "../syscall/event"; import {eventSyscalls} from "../syscalls/event";
let args = yargs(hideBin(process.argv)) let args = yargs(hideBin(process.argv))
.option("port", { .option("port", {
@ -35,7 +35,7 @@ const plugPath = args._[0] as string;
const app = express(); const app = express();
type ServerHook = EndpointHook & CronHook & EventHook; type ServerHook = EndpointHookT & CronHookT & EventHookT;
const system = new System<ServerHook>("server"); const system = new System<ServerHook>("server");
safeRun(async () => { safeRun(async () => {
@ -52,11 +52,11 @@ safeRun(async () => {
let plugLoader = new DiskPlugLoader(system, plugPath); let plugLoader = new DiskPlugLoader(system, plugPath);
await plugLoader.loadPlugs(); await plugLoader.loadPlugs();
plugLoader.watcher(); plugLoader.watcher();
system.addFeature(new NodeCronFeature()); system.addHook(new NodeCronHook());
let eventFeature = new EventFeature(); let eventHook = new EventHook();
system.addFeature(eventFeature); system.addHook(eventHook);
system.registerSyscalls("event", [], eventSyscalls(eventFeature)); system.registerSyscalls("event", [], eventSyscalls(eventHook));
system.addFeature(new EndpointFeature(app, "")); system.addHook(new EndpointHook(app, ""));
system.registerSyscalls("shell", [], shellSyscalls(".")); system.registerSyscalls("shell", [], shellSyscalls("."));
system.registerSyscalls("fetch", [], fetchSyscalls()); system.registerSyscalls("fetch", [], fetchSyscalls());
system.registerSyscalls( system.registerSyscalls(

View File

@ -50,7 +50,6 @@ parentPort.on("message", (data: any) => {
safeRun(async () => { safeRun(async () => {
switch (data.type) { switch (data.type) {
case "load": case "load":
console.log("Booting", data.name);
loadedFunctions.set(data.name, new VMScript(wrapScript(data.code))); loadedFunctions.set(data.name, new VMScript(wrapScript(data.code)));
parentPort.postMessage({ parentPort.postMessage({
type: "inited", type: "inited",

View File

@ -48,7 +48,6 @@ self.addEventListener("message", (event: { data: WorkerMessage }) => {
let data = messageEvent.data; let data = messageEvent.data;
switch (data.type) { switch (data.type) {
case "load": case "load":
console.log("Booting", data.name);
loadedFunctions.set(data.name!, new Function(wrapScript(data.code!))); loadedFunctions.set(data.name!, new Function(wrapScript(data.code!)));
postMessage( postMessage(
{ {

View File

@ -1,13 +1,13 @@
import { createSandbox } from "../environment/node_sandbox"; import { createSandbox } from "../environments/node_sandbox";
import { expect, test } from "@jest/globals"; import { expect, test } from "@jest/globals";
import { Manifest } from "../types"; import { Manifest } from "../types";
import express from "express"; import express from "express";
import request from "supertest"; import request from "supertest";
import { EndpointFeature, EndpointHook } from "./endpoint"; import { EndpointHook, EndpointHookT } from "./endpoint";
import { System } from "../system"; import { System } from "../system";
test("Run a plugos endpoint server", async () => { test("Run a plugos endpoint server", async () => {
let system = new System<EndpointHook>("server"); let system = new System<EndpointHookT>("server");
let plug = await system.load( let plug = await system.load(
"test", "test",
{ {
@ -26,14 +26,14 @@ test("Run a plugos endpoint server", async () => {
})()`, })()`,
}, },
}, },
} as Manifest<EndpointHook>, } as Manifest<EndpointHookT>,
createSandbox createSandbox
); );
const app = express(); const app = express();
const port = 3123; const port = 3123;
system.addFeature(new EndpointFeature(app, "/_")); system.addHook(new EndpointHook(app, "/_"));
let server = app.listen(port, () => { let server = app.listen(port, () => {
console.log(`Listening on port ${port}`); console.log(`Listening on port ${port}`);

View File

@ -1,4 +1,4 @@
import { Feature, Manifest } from "../types"; import { Hook, Manifest } from "../types";
import { Express, NextFunction, Request, Response } from "express"; import { Express, NextFunction, Request, Response } from "express";
import { System } from "../system"; import { System } from "../system";
@ -16,7 +16,7 @@ export type EndpointResponse = {
body: any; body: any;
}; };
export type EndpointHook = { export type EndpointHookT = {
http?: EndPointDef | EndPointDef[]; http?: EndPointDef | EndPointDef[];
}; };
@ -25,7 +25,7 @@ export type EndPointDef = {
path: string; path: string;
}; };
export class EndpointFeature implements Feature<EndpointHook> { export class EndpointHook implements Hook<EndpointHookT> {
private app: Express; private app: Express;
private prefix: string; private prefix: string;
@ -34,7 +34,7 @@ export class EndpointFeature implements Feature<EndpointHook> {
this.prefix = prefix; this.prefix = prefix;
} }
apply(system: System<EndpointHook>): void { apply(system: System<EndpointHookT>): void {
this.app.use((req: Request, res: Response, next: NextFunction) => { this.app.use((req: Request, res: Response, next: NextFunction) => {
if (!req.path.startsWith(this.prefix)) { if (!req.path.startsWith(this.prefix)) {
return next(); return next();
@ -106,7 +106,7 @@ export class EndpointFeature implements Feature<EndpointHook> {
}); });
} }
validateManifest(manifest: Manifest<EndpointHook>): string[] { validateManifest(manifest: Manifest<EndpointHookT>): string[] {
let errors = []; let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) { for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (!functionDef.http) { if (!functionDef.http) {

View File

@ -1,19 +1,19 @@
import { Feature, Manifest } from "../types"; import { Hook, Manifest } from "../types";
import { System } from "../system"; import { System } from "../system";
// System events: // System events:
// - plug:load (plugName: string) // - plug:load (plugName: string)
export type EventHook = { export type EventHookT = {
events?: string[]; events?: string[];
}; };
export class EventFeature implements Feature<EventHook> { export class EventHook implements Hook<EventHookT> {
private system?: System<EventHook>; private system?: System<EventHookT>;
async dispatchEvent(eventName: string, data?: any): Promise<any[]> { async dispatchEvent(eventName: string, data?: any): Promise<any[]> {
if (!this.system) { if (!this.system) {
throw new Error("EventFeature is not initialized"); throw new Error("Event hook is not initialized");
} }
let promises: Promise<any>[] = []; let promises: Promise<any>[] = [];
for (const plug of this.system.loadedPlugs.values()) { for (const plug of this.system.loadedPlugs.values()) {
@ -31,7 +31,7 @@ export class EventFeature implements Feature<EventHook> {
return Promise.all(promises); return Promise.all(promises);
} }
apply(system: System<EventHook>): void { apply(system: System<EventHookT>): void {
this.system = system; this.system = system;
this.system.on({ this.system.on({
plugLoaded: (name) => { plugLoaded: (name) => {
@ -40,7 +40,7 @@ export class EventFeature implements Feature<EventHook> {
}); });
} }
validateManifest(manifest: Manifest<EventHook>): string[] { validateManifest(manifest: Manifest<EventHookT>): string[] {
let errors = []; let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) { for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (functionDef.events && !Array.isArray(functionDef.events)) { if (functionDef.events && !Array.isArray(functionDef.events)) {

View File

@ -1,14 +1,14 @@
import { Feature, Manifest } from "../types"; import { Hook, Manifest } from "../types";
import cron, { ScheduledTask } from "node-cron"; import cron, { ScheduledTask } from "node-cron";
import { safeRun } from "../util"; import { safeRun } from "../util";
import { System } from "../system"; import { System } from "../system";
export type CronHook = { export type CronHookT = {
cron?: string | string[]; cron?: string | string[];
}; };
export class NodeCronFeature implements Feature<CronHook> { export class NodeCronHook implements Hook<CronHookT> {
apply(system: System<CronHook>): void { apply(system: System<CronHookT>): void {
let tasks: ScheduledTask[] = []; let tasks: ScheduledTask[] = [];
system.on({ system.on({
plugLoaded: (name, plug) => { plugLoaded: (name, plug) => {
@ -56,7 +56,7 @@ export class NodeCronFeature implements Feature<CronHook> {
} }
} }
validateManifest(manifest: Manifest<CronHook>): string[] { validateManifest(manifest: Manifest<CronHookT>): string[] {
let errors = []; let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) { for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (!functionDef.cron) { if (!functionDef.cron) {

View File

@ -10,7 +10,7 @@
"watch": "rm -rf .parcel-cache && parcel watch", "watch": "rm -rf .parcel-cache && parcel watch",
"build": "parcel build", "build": "parcel build",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"test": "jest dist" "test": "jest dist/test"
}, },
"files": [ "files": [
"dist" "dist"
@ -28,9 +28,9 @@
"test": { "test": {
"source": [ "source": [
"runtime.test.ts", "runtime.test.ts",
"feature/endpoint.test.ts", "hooks/endpoint.test.ts",
"syscall/store.knex_node.test.ts", "syscalls/store.knex_node.test.ts",
"syscall/store.dexie_browser.test.ts" "syscalls/store.dexie_browser.test.ts"
], ],
"outputFormat": "commonjs", "outputFormat": "commonjs",
"isLibrary": true, "isLibrary": true,

View File

@ -1,7 +1,7 @@
import fs from "fs/promises"; import fs from "fs/promises";
import watch from "node-watch"; import watch from "node-watch";
import path from "path"; import path from "path";
import { createSandbox } from "./environment/node_sandbox"; import { createSandbox } from "./environments/node_sandbox";
import { safeRun } from "../server/util"; import { safeRun } from "../server/util";
import { System } from "./system"; import { System } from "./system";

View File

@ -1,4 +1,4 @@
import { createSandbox } from "./environment/node_sandbox"; import { createSandbox } from "./environments/node_sandbox";
import { expect, test } from "@jest/globals"; import { expect, test } from "@jest/globals";
import { System } from "./system"; import { System } from "./system";

View File

@ -2,7 +2,7 @@ import {
ControllerMessage, ControllerMessage,
WorkerLike, WorkerLike,
WorkerMessage, WorkerMessage,
} from "./environment/worker"; } from "./environments/worker";
import { Plug } from "./plug"; import { Plug } from "./plug";
export type SandboxFactory<HookT> = (plug: Plug<HookT>) => Sandbox; export type SandboxFactory<HookT> = (plug: Plug<HookT>) => Sandbox;

View File

@ -1,10 +0,0 @@
import { SysCallMapping } from "../system";
import { EventFeature } from "../feature/event";
export function eventSyscalls(eventFeature: EventFeature): SysCallMapping {
return {
async dispatch(ctx, eventName: string, data: any) {
return eventFeature.dispatchEvent(eventName, data);
},
};
}

10
plugos/syscalls/event.ts Normal file
View File

@ -0,0 +1,10 @@
import { SysCallMapping } from "../system";
import { EventHook } from "../hooks/event";
export function eventSyscalls(eventHook: EventHook): SysCallMapping {
return {
async dispatch(ctx, eventName: string, data: any) {
return eventHook.dispatchEvent(eventName, data);
},
};
}

View File

@ -1,4 +1,4 @@
import { createSandbox } from "../environment/node_sandbox"; import { createSandbox } from "../environments/node_sandbox";
import { expect, test } from "@jest/globals"; import { expect, test } from "@jest/globals";
import { System } from "../system"; import { System } from "../system";
import { storeSyscalls } from "./store.dexie_browser"; import { storeSyscalls } from "./store.dexie_browser";

View File

@ -1,4 +1,4 @@
import { createSandbox } from "../environment/node_sandbox"; import { createSandbox } from "../environments/node_sandbox";
import { expect, test } from "@jest/globals"; import { expect, test } from "@jest/globals";
import { System } from "../system"; import { System } from "../system";
import { import {

View File

@ -1,4 +1,4 @@
import { Feature, Manifest, RuntimeEnvironment } from "./types"; import { Hook, Manifest, RuntimeEnvironment } from "./types";
import { EventEmitter } from "../common/event"; import { EventEmitter } from "../common/event";
import { SandboxFactory } from "./sandbox"; import { SandboxFactory } from "./sandbox";
import { Plug } from "./plug"; import { Plug } from "./plug";
@ -31,7 +31,7 @@ type Syscall = {
export class System<HookT> extends EventEmitter<SystemEvents<HookT>> { export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
protected plugs = new Map<string, Plug<HookT>>(); protected plugs = new Map<string, Plug<HookT>>();
protected registeredSyscalls = new Map<string, Syscall>(); protected registeredSyscalls = new Map<string, Syscall>();
protected enabledFeatures = new Set<Feature<HookT>>(); protected enabledHooks = new Set<Hook<HookT>>();
readonly runtimeEnv: RuntimeEnvironment; readonly runtimeEnv: RuntimeEnvironment;
@ -40,8 +40,8 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
this.runtimeEnv = env; this.runtimeEnv = env;
} }
addFeature(feature: Feature<HookT>) { addHook(feature: Hook<HookT>) {
this.enabledFeatures.add(feature); this.enabledHooks.add(feature);
feature.apply(this); feature.apply(this);
} }
@ -91,7 +91,7 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
} }
// Validate // Validate
let errors: string[] = []; let errors: string[] = [];
for (const feature of this.enabledFeatures) { for (const feature of this.enabledHooks) {
errors = [...errors, ...feature.validateManifest(manifest)]; errors = [...errors, ...feature.validateManifest(manifest)];
} }
if (errors.length > 0) { if (errors.length > 0) {

View File

@ -1,5 +1,5 @@
{ {
"include": ["bin/*", "environment/*", "feature/**", "syscall/*", "*"], "include": ["bin/*", "environments/*", "hooks/**", "syscalls/*", "*"],
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "esnext",
"strict": true, "strict": true,

View File

@ -15,7 +15,7 @@ export type FunctionDef<HookT> = {
export type RuntimeEnvironment = "client" | "server"; export type RuntimeEnvironment = "client" | "server";
export interface Feature<HookT> { export interface Hook<HookT> {
validateManifest(manifest: Manifest<HookT>): string[]; validateManifest(manifest: Manifest<HookT>): string[];
apply(system: System<HookT>): void; apply(system: System<HookT>): void;

View File

@ -44,15 +44,10 @@ functions:
- page:click - page:click
insertToday: insertToday:
path: "./dates.ts:insertToday" path: "./dates.ts:insertToday"
command: slashCommand:
name: Insert Current Date name: today
slashCommand: today
welcome: welcome:
path: "./server.ts:welcome" path: "./server.ts:welcome"
events: events:
- plug:load - plug:load
env: server env: server
# renderMD:
# path: "./markdown.ts:renderMD"
# command:
# name: Render Markdown

View File

@ -1,7 +1,4 @@
import { import { EndpointRequest, EndpointResponse } from "../../plugos/hooks/endpoint";
EndpointRequest,
EndpointResponse,
} from "../../plugos/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);

View File

@ -1,6 +1,6 @@
import { Express } from "express"; import { Express } from "express";
import { SilverBulletHooks } from "../common/manifest"; import { SilverBulletHooks } from "../common/manifest";
import { EndpointFeature } from "../plugos/feature/endpoint"; import { EndpointHook } from "../plugos/hooks/endpoint";
import { readFile } from "fs/promises"; import { readFile } from "fs/promises";
import { System } from "../plugos/system"; import { System } from "../plugos/system";
@ -19,7 +19,7 @@ export class ExpressServer {
this.rootPath = rootPath; this.rootPath = rootPath;
this.system = system; this.system = system;
system.addFeature(new EndpointFeature(app, "/_")); system.addHook(new EndpointHook(app, "/_"));
// Fallback, serve index.html // Fallback, serve index.html
let cachedIndex: string | undefined = undefined; let cachedIndex: string | undefined = undefined;

View File

@ -11,9 +11,9 @@ import { stat } from "fs/promises";
import { Cursor, cursorEffect } from "../webapp/cursorEffect"; import { Cursor, cursorEffect } from "../webapp/cursorEffect";
import { SilverBulletHooks } from "../common/manifest"; import { SilverBulletHooks } from "../common/manifest";
import { System } from "../plugos/system"; import { System } from "../plugos/system";
import { EventFeature } from "../plugos/feature/event"; import { EventHook } from "../plugos/hooks/event";
import spaceSyscalls from "./syscalls/space"; import spaceSyscalls from "./syscalls/space";
import { eventSyscalls } from "../plugos/syscall/event"; import { eventSyscalls } from "../plugos/syscalls/event";
export class PageApi implements ApiProvider { export class PageApi implements ApiProvider {
openPages: Map<string, Page>; openPages: Map<string, Page>;
@ -21,7 +21,7 @@ export class PageApi implements ApiProvider {
rootPath: string; rootPath: string;
connectedSockets: Set<Socket>; connectedSockets: Set<Socket>;
private system: System<SilverBulletHooks>; private system: System<SilverBulletHooks>;
private eventFeature: EventFeature; private eventHook: EventHook;
constructor( constructor(
rootPath: string, rootPath: string,
@ -34,10 +34,10 @@ export class PageApi implements ApiProvider {
this.openPages = openPages; this.openPages = openPages;
this.connectedSockets = connectedSockets; this.connectedSockets = connectedSockets;
this.system = system; this.system = system;
this.eventFeature = new EventFeature(); this.eventHook = new EventHook();
system.addFeature(this.eventFeature); system.addHook(this.eventHook);
system.registerSyscalls("space", [], spaceSyscalls(this)); system.registerSyscalls("space", [], spaceSyscalls(this));
system.registerSyscalls("event", [], eventSyscalls(this.eventFeature)); system.registerSyscalls("event", [], eventSyscalls(this.eventHook));
} }
async init(): Promise<void> { async init(): Promise<void> {
@ -229,11 +229,8 @@ export class PageApi implements ApiProvider {
" to disk and indexing." " to disk and indexing."
); );
await this.flushPageToDisk(pageName, page); await this.flushPageToDisk(pageName, page);
await this.eventFeature.dispatchEvent( await this.eventHook.dispatchEvent("page:saved", pageName);
"page:saved", await this.eventHook.dispatchEvent("page:index", {
pageName
);
await this.eventFeature.dispatchEvent("page:index", {
name: pageName, name: pageName,
text: page.text.sliceString(0), text: page.text.sliceString(0),
}); });
@ -312,8 +309,8 @@ export class PageApi implements ApiProvider {
this.openPages.delete(pageName); this.openPages.delete(pageName);
} }
// Trigger system events // Trigger system events
await this.eventFeature.dispatchEvent("page:saved", pageName); await this.eventHook.dispatchEvent("page:saved", pageName);
await this.eventFeature.dispatchEvent("page:index", { await this.eventHook.dispatchEvent("page:index", {
name: pageName, name: pageName,
text: text, text: text,
}); });
@ -325,7 +322,7 @@ export class PageApi implements ApiProvider {
clientConn.openPages.delete(pageName); clientConn.openPages.delete(pageName);
// Cascading of this to all connected clients will be handled by file watcher // Cascading of this to all connected clients will be handled by file watcher
await this.pageStore.deletePage(pageName); await this.pageStore.deletePage(pageName);
await this.eventFeature.dispatchEvent("page:deleted", pageName); await this.eventHook.dispatchEvent("page:deleted", pageName);
}, },
listPages: async (clientConn: ClientConnection): Promise<PageMeta[]> => { listPages: async (clientConn: ClientConnection): Promise<PageMeta[]> => {

View File

@ -9,8 +9,8 @@ import {hideBin} from "yargs/helpers";
import {SilverBulletHooks} from "../common/manifest"; import {SilverBulletHooks} from "../common/manifest";
import {ExpressServer} from "./express_server"; import {ExpressServer} from "./express_server";
import {DiskPlugLoader} from "../plugos/plug_loader"; import {DiskPlugLoader} from "../plugos/plug_loader";
import {NodeCronFeature} from "../plugos/feature/node_cron"; import {NodeCronHook} from "../plugos/hooks/node_cron";
import shellSyscalls from "../plugos/syscall/shell.node"; import shellSyscalls from "../plugos/syscalls/shell.node";
import {System} from "../plugos/system"; import {System} from "../plugos/system";
let args = yargs(hideBin(process.argv)) let args = yargs(hideBin(process.argv))
@ -59,7 +59,7 @@ expressServer
await plugLoader.loadPlugs(); await plugLoader.loadPlugs();
plugLoader.watcher(); plugLoader.watcher();
system.registerSyscalls("shell", ["shell"], shellSyscalls(pagesPath)); system.registerSyscalls("shell", ["shell"], shellSyscalls(pagesPath));
system.addFeature(new NodeCronFeature()); system.addHook(new NodeCronHook());
server.listen(port, () => { server.listen(port, () => {
console.log(`Server listening on port ${port}`); console.log(`Server listening on port ${port}`);
}); });

View File

@ -5,7 +5,7 @@ import {
ensureTable, ensureTable,
storeReadSyscalls, storeReadSyscalls,
storeWriteSyscalls, storeWriteSyscalls,
} from "../../plugos/syscall/store.knex_node"; } from "../../plugos/syscalls/store.knex_node";
type IndexItem = { type IndexItem = {
page: string; page: string;

View File

@ -1,7 +1,7 @@
import { AppCommand } from "../types";
import { isMacLike } from "../util"; import { isMacLike } from "../util";
import { FilterList, Option } from "./filter"; import { FilterList, Option } from "./filter";
import { faPersonRunning } from "@fortawesome/free-solid-svg-icons"; import { faPersonRunning } from "@fortawesome/free-solid-svg-icons";
import { AppCommand } from "../hooks/command";
export function CommandPalette({ export function CommandPalette({
commands, commands,
@ -18,7 +18,6 @@ export function CommandPalette({
hint: isMac && def.command.mac ? def.command.mac : def.command.key, hint: isMac && def.command.mac ? def.command.mac : def.command.key,
}); });
} }
console.log("Commands", options);
return ( return (
<FilterList <FilterList
label="Run" label="Run"

View File

@ -1,7 +1,5 @@
import { import {
autocompletion, autocompletion,
Completion,
CompletionContext,
completionKeymap, completionKeymap,
CompletionResult, CompletionResult,
} from "@codemirror/autocomplete"; } from "@codemirror/autocomplete";
@ -21,7 +19,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 { createSandbox as createIFrameSandbox } from "../plugos/environment/iframe_sandbox"; import { createSandbox as createIFrameSandbox } from "../plugos/environments/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";
@ -40,19 +38,15 @@ import customMarkdownStyle from "./style";
import editorSyscalls from "./syscalls/editor"; import editorSyscalls from "./syscalls/editor";
import indexerSyscalls from "./syscalls/indexer"; import indexerSyscalls from "./syscalls/indexer";
import spaceSyscalls from "./syscalls/space"; import spaceSyscalls from "./syscalls/space";
import { import { Action, AppViewState, initialViewState } from "./types";
Action,
AppCommand,
AppViewState,
initialViewState,
slashCommandRegexp,
} from "./types";
import { SilverBulletHooks } from "../common/manifest"; import { SilverBulletHooks } from "../common/manifest";
import { safeRun } from "./util"; import { safeRun } from "./util";
import { System } from "../plugos/system"; import { System } from "../plugos/system";
import { EventFeature } from "../plugos/feature/event"; import { EventHook } from "../plugos/hooks/event";
import { systemSyscalls } from "./syscalls/system"; import { systemSyscalls } from "./syscalls/system";
import { Panel } from "./components/panel"; import { Panel } from "./components/panel";
import { CommandHook } from "./hooks/command";
import { SlashCommandHook } from "./hooks/slash_command";
class PageState { class PageState {
scrollTop: number; scrollTop: number;
@ -67,22 +61,39 @@ class PageState {
export class Editor implements AppEventDispatcher { export class Editor implements AppEventDispatcher {
private system = new System<SilverBulletHooks>("client"); private system = new System<SilverBulletHooks>("client");
openPages = new Map<string, PageState>(); openPages = new Map<string, PageState>();
editorCommands = new Map<string, AppCommand>(); commandHook: CommandHook;
editorView?: EditorView; editorView?: EditorView;
viewState: AppViewState; viewState: AppViewState;
viewDispatch: React.Dispatch<Action>; viewDispatch: React.Dispatch<Action>;
space: Space; space: Space;
navigationResolve?: (val: undefined) => void;
pageNavigator: PathPageNavigator; pageNavigator: PathPageNavigator;
private eventFeature: EventFeature; eventHook: EventHook;
private slashCommandHook: SlashCommandHook;
constructor(space: Space, parent: Element) { constructor(space: Space, parent: Element) {
this.space = space; this.space = space;
this.viewState = initialViewState; this.viewState = initialViewState;
this.viewDispatch = () => {}; this.viewDispatch = () => {};
this.eventFeature = new EventFeature(); // Event hook
this.system.addFeature(this.eventFeature); this.eventHook = new EventHook();
this.system.addHook(this.eventHook);
// Command hook
this.commandHook = new CommandHook();
this.commandHook.on({
commandsUpdated: (commandMap) => {
this.viewDispatch({
type: "update-commands",
commands: commandMap,
});
},
});
this.system.addHook(this.commandHook);
// Slash command hook
this.slashCommandHook = new SlashCommandHook(this);
this.system.addHook(this.slashCommandHook);
this.render(parent); this.render(parent);
this.editorView = new EditorView({ this.editorView = new EditorView({
@ -142,14 +153,12 @@ export class Editor implements AppEventDispatcher {
loadSystem: (systemJSON) => { loadSystem: (systemJSON) => {
safeRun(async () => { safeRun(async () => {
await this.system.replaceAllFromJSON(systemJSON, createIFrameSandbox); await this.system.replaceAllFromJSON(systemJSON, createIFrameSandbox);
this.buildAllCommands();
}); });
}, },
plugLoaded: (plugName, plug) => { plugLoaded: (plugName, plug) => {
safeRun(async () => { safeRun(async () => {
console.log("Plug load", plugName); console.log("Plug load", plugName);
await this.system.load(plugName, plug, createIFrameSandbox); await this.system.load(plugName, plug, createIFrameSandbox);
this.buildAllCommands();
}); });
}, },
plugUnloaded: (plugName) => { plugUnloaded: (plugName) => {
@ -161,39 +170,10 @@ export class Editor implements AppEventDispatcher {
}); });
if (this.pageNavigator.getCurrentPage() === "") { if (this.pageNavigator.getCurrentPage() === "") {
this.pageNavigator.navigate("start"); await this.pageNavigator.navigate("start");
} }
} }
private buildAllCommands() {
console.log("Loaded plugs, now updating editor commands");
this.editorCommands.clear();
for (let plug of this.system.loadedPlugs.values()) {
for (const [name, functionDef] of Object.entries(
plug.manifest!.functions
)) {
if (!functionDef.command) {
continue;
}
const cmds = Array.isArray(functionDef.command)
? functionDef.command
: [functionDef.command];
for (let cmd of cmds) {
this.editorCommands.set(cmd.name, {
command: cmd,
run: () => {
return plug.invoke(name, []);
},
});
}
}
}
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({
@ -213,7 +193,7 @@ export class Editor implements AppEventDispatcher {
} }
async dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]> { async dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]> {
return this.eventFeature.dispatchEvent(name, data); return this.eventHook.dispatchEvent(name, data);
} }
get currentPage(): string | undefined { get currentPage(): string | undefined {
@ -222,7 +202,7 @@ export class Editor implements AppEventDispatcher {
createEditorState(pageName: string, doc: CollabDocument): EditorState { createEditorState(pageName: string, doc: CollabDocument): EditorState {
let commandKeyBindings: KeyBinding[] = []; let commandKeyBindings: KeyBinding[] = [];
for (let def of this.editorCommands.values()) { for (let def of this.commandHook.editorCommands.values()) {
if (def.command.key) { if (def.command.key) {
commandKeyBindings.push({ commandKeyBindings.push({
key: def.command.key, key: def.command.key,
@ -257,7 +237,9 @@ export class Editor implements AppEventDispatcher {
autocompletion({ autocompletion({
override: [ override: [
this.plugCompleter.bind(this), this.plugCompleter.bind(this),
this.commandCompleter.bind(this), this.slashCommandHook.slashCommandCompleter.bind(
this.slashCommandHook
),
], ],
}), }),
EditorView.lineWrapping, EditorView.lineWrapping,
@ -361,39 +343,6 @@ export class Editor implements AppEventDispatcher {
return null; return null;
} }
commandCompleter(ctx: CompletionContext): CompletionResult | null {
let prefix = ctx.matchBefore(slashCommandRegexp);
if (!prefix) {
return null;
}
let options: Completion[] = [];
for (let [name, def] of this.viewState.commands) {
if (!def.command.slashCommand) {
continue;
}
options.push({
label: def.command.slashCommand,
detail: name,
apply: () => {
this.editorView?.dispatch({
changes: {
from: prefix!.from,
to: ctx.pos,
insert: "",
},
});
safeRun(async () => {
await def.run();
});
},
});
}
return {
from: prefix.from + 1,
options: options,
};
}
focus() { focus() {
this.editorView!.focus(); this.editorView!.focus();
} }
@ -469,7 +418,7 @@ export class Editor implements AppEventDispatcher {
editor.focus(); editor.focus();
if (page) { if (page) {
safeRun(async () => { safeRun(async () => {
editor.navigate(page); await editor.navigate(page);
}); });
} }
}} }}
@ -497,7 +446,7 @@ export class Editor implements AppEventDispatcher {
dispatch({ type: "start-navigate" }); dispatch({ type: "start-navigate" });
}} }}
/> />
<div id="editor"></div> <div id="editor" />
</div> </div>
); );
} }

75
webapp/hooks/command.ts Normal file
View File

@ -0,0 +1,75 @@
import { Hook, Manifest } from "../../plugos/types";
import { System } from "../../plugos/system";
import { EventEmitter } from "../../common/event";
export type CommandDef = {
name: string;
// Bind to keyboard shortcut
key?: string;
mac?: string;
};
export type AppCommand = {
command: CommandDef;
run: () => Promise<void>;
};
export type CommandHookT = {
command?: CommandDef;
};
export type CommandHookEvents = {
commandsUpdated(commandMap: Map<string, AppCommand>): void;
};
export class CommandHook
extends EventEmitter<CommandHookEvents>
implements Hook<CommandHookT>
{
editorCommands = new Map<string, AppCommand>();
buildAllCommands(system: System<CommandHookT>) {
this.editorCommands.clear();
for (let plug of system.loadedPlugs.values()) {
for (const [name, functionDef] of Object.entries(
plug.manifest!.functions
)) {
if (!functionDef.command) {
continue;
}
const cmd = functionDef.command;
this.editorCommands.set(cmd.name, {
command: cmd,
run: () => {
return plug.invoke(name, []);
},
});
}
}
this.emit("commandsUpdated", this.editorCommands);
}
apply(system: System<CommandHookT>): void {
this.buildAllCommands(system);
system.on({
plugLoaded: () => {
this.buildAllCommands(system);
},
});
}
validateManifest(manifest: Manifest<CommandHookT>): string[] {
let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (!functionDef.command) {
continue;
}
const cmd = functionDef.command;
if (!cmd.name) {
errors.push(`Function ${name} has a command but no name`);
}
}
return [];
}
}

View File

@ -0,0 +1,111 @@
import { Hook, Manifest } from "../../plugos/types";
import { System } from "../../plugos/system";
import {
Completion,
CompletionContext,
CompletionResult,
} from "@codemirror/autocomplete";
import { slashCommandRegexp } from "../types";
import { safeRun } from "../util";
import { Editor } from "../editor";
export type SlashCommandDef = {
name: string;
};
export type AppSlashCommand = {
slashCommand: SlashCommandDef;
run: () => Promise<void>;
};
export type SlashCommandHookT = {
slashCommand?: SlashCommandDef;
};
export class SlashCommandHook implements Hook<SlashCommandHookT> {
slashCommands = new Map<string, AppSlashCommand>();
private editor: Editor;
constructor(editor: Editor) {
this.editor = editor;
}
buildAllCommands(system: System<SlashCommandHookT>) {
this.slashCommands.clear();
for (let plug of system.loadedPlugs.values()) {
for (const [name, functionDef] of Object.entries(
plug.manifest!.functions
)) {
if (!functionDef.slashCommand) {
continue;
}
const cmd = functionDef.slashCommand;
this.slashCommands.set(cmd.name, {
slashCommand: cmd,
run: () => {
return plug.invoke(name, []);
},
});
}
}
}
// Completer for CodeMirror
public slashCommandCompleter(
ctx: CompletionContext
): CompletionResult | null {
let prefix = ctx.matchBefore(slashCommandRegexp);
if (!prefix) {
return null;
}
let options: Completion[] = [];
for (let [name, def] of this.slashCommands.entries()) {
options.push({
label: def.slashCommand.name,
detail: name,
apply: () => {
// Delete slash command part
this.editor.editorView?.dispatch({
changes: {
from: prefix!.from,
to: ctx.pos,
insert: "",
},
});
// Replace with whatever the completion is
safeRun(async () => {
await def.run();
});
},
});
}
return {
// + 1 because of the '/'
from: prefix.from + 1,
options: options,
};
}
apply(system: System<SlashCommandHookT>): void {
this.buildAllCommands(system);
system.on({
plugLoaded: () => {
this.buildAllCommands(system);
},
});
}
validateManifest(manifest: Manifest<SlashCommandHookT>): string[] {
let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (!functionDef.slashCommand) {
continue;
}
const cmd = functionDef.slashCommand;
if (!cmd.name) {
errors.push(`Function ${name} has a command but no name`);
}
}
return [];
}
}

View File

@ -1,6 +1,6 @@
import { Space } from "../space"; import { Space } from "../space";
import { SysCallMapping } from "../../plugos/system"; import { SysCallMapping } from "../../plugos/system";
import { transportSyscalls } from "../../plugos/syscall/transport"; import { transportSyscalls } from "../../plugos/syscalls/transport";
export default function indexerSyscalls(space: Space): SysCallMapping { export default function indexerSyscalls(space: Space): SysCallMapping {
return transportSyscalls( return transportSyscalls(

View File

@ -1,4 +1,4 @@
import { CommandDef } from "../common/manifest"; import { AppCommand } from "./hooks/command";
export type PageMeta = { export type PageMeta = {
name: string; name: string;
@ -7,11 +7,6 @@ export type PageMeta = {
lastOpened?: number; lastOpened?: number;
}; };
export type AppCommand = {
command: CommandDef;
run: () => Promise<void>;
};
export const slashCommandRegexp = /\/[\w\-]*/; export const slashCommandRegexp = /\/[\w\-]*/;
export type Notification = { export type Notification = {