Refactoring

pull/3/head
Zef Hemel 2022-03-25 12:03:06 +01:00
parent b51730d33f
commit 08e6c3bad8
52 changed files with 796 additions and 326 deletions

View File

@ -1,6 +1,7 @@
import * as plugbox from "../plugbox/types";
import { EndpointHook } from "../plugbox/feature/endpoint";
import { CronHook } from "../plugbox/feature/node_cron";
import { EventHook } from "../plugbox/feature/event";
export type CommandDef = {
// Function name to invoke
@ -20,6 +21,7 @@ export type SilverBulletHooks = {
[key: string]: CommandDef;
};
} & EndpointHook &
CronHook;
CronHook &
EventHook;
export type Manifest = plugbox.Manifest<SilverBulletHooks>;

View File

@ -12,7 +12,7 @@
"watch": "rm -rf .parcel-cache && parcel watch",
"build": "parcel build",
"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 && node dist/bundler/plugbox-bundle.js plugs/git/git.plug.json plugs/dist/git.plug.json",
"server": "nodemon -w dist/server dist/server/server.js pages",
"test": "jest"
},
@ -41,6 +41,8 @@
"source": [
"plugbox/runtime.test.ts",
"plugbox/feature/endpoint.test.ts",
"plugbox/syscall/store.knex_node.test.ts",
"plugbox/syscall/store.dexie_browser.test.ts",
"server/api.test.ts"
],
"outputFormat": "commonjs",
@ -72,11 +74,13 @@
"dexie": "^3.2.1",
"esbuild": "^0.14.27",
"express": "^4.17.3",
"fake-indexeddb": "^3.1.7",
"idb": "^7.0.0",
"jest": "^27.5.1",
"knex": "^1.0.4",
"lodash": "^4.17.21",
"node-cron": "^3.0.0",
"node-fetch": "^3.2.3",
"nodemon": "^2.0.15",
"parcel": "^2.3.2",
"react": "^17.0.2",
@ -99,6 +103,7 @@
"@types/jest": "^27.4.1",
"@types/node": "^17.0.21",
"@types/node-cron": "^3.0.1",
"@types/node-fetch": "^2.6.1",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"@types/supertest": "^2.0.11",

View File

@ -3,8 +3,8 @@ import { safeRun } from "../util";
// @ts-ignore
import sandboxHtml from "bundle-text:./iframe_sandbox.html";
import { Sandbox } from "../sandbox";
import { System } from "../system";
import { WorkerLike } from "./worker";
import { Plug } from "../plug";
class IFrameWrapper implements WorkerLike {
private iframe: HTMLIFrameElement;
@ -49,6 +49,6 @@ class IFrameWrapper implements WorkerLike {
}
}
export function createSandbox(system: System<any>) {
return new Sandbox(system, new IFrameWrapper());
export function createSandbox(plug: Plug<any>) {
return new Sandbox(plug, new IFrameWrapper());
}

View File

@ -4,8 +4,8 @@ import { safeRun } from "../util";
// @ts-ignore
import workerCode from "bundle-text:./node_worker.ts";
import { Sandbox } from "../sandbox";
import { System } from "../system";
import { WorkerLike } from "./worker";
import { Plug } from "../plug";
class NodeWorkerWrapper implements WorkerLike {
onMessage?: (message: any) => Promise<void>;
@ -33,12 +33,12 @@ class NodeWorkerWrapper implements WorkerLike {
}
}
export function createSandbox(system: System<any>) {
export function createSandbox(plug: Plug<any>) {
let worker = new Worker(workerCode, {
eval: true,
});
return new Sandbox(
system,
plug,
new NodeWorkerWrapper(
new Worker(workerCode, {
eval: true,

View File

@ -1,7 +1,7 @@
import { safeRun } from "../util";
import { Sandbox } from "../sandbox";
import { System } from "../system";
import { WorkerLike } from "./worker";
import { Plug } from "../plug";
class WebWorkerWrapper implements WorkerLike {
private worker: Worker;
@ -28,10 +28,10 @@ class WebWorkerWrapper implements WorkerLike {
}
}
export function createSandbox(system: System<any>) {
export function createSandbox(plug: Plug<any>) {
// ParcelJS will build this file into a worker.
let worker = new Worker(new URL("sandbox_worker.ts", import.meta.url), {
type: "module",
});
return new Sandbox(system, new WebWorkerWrapper(worker));
return new Sandbox(plug, new WebWorkerWrapper(worker));
}

View File

@ -27,7 +27,7 @@ test("Run a plugbox endpoint server", async () => {
endpoints: [{ method: "GET", path: "/", handler: "testhandler" }],
},
} as Manifest<EndpointHook>,
createSandbox(system)
createSandbox
);
const app = express();

43
plugbox/feature/event.ts Normal file
View File

@ -0,0 +1,43 @@
import { Feature, Manifest } from "../types";
import { System } from "../system";
export type EventHook = {
events?: { [key: string]: string[] };
};
export class EventFeature implements Feature<EventHook> {
private system?: System<EventHook>;
async dispatchEvent(name: string, data?: any): Promise<any[]> {
if (!this.system) {
throw new Error("EventFeature is not initialized");
}
let promises: Promise<any>[] = [];
for (const plug of this.system.loadedPlugs.values()) {
if (!plug.manifest!.hooks?.events) {
continue;
}
let functionsToSpawn = plug.manifest!.hooks.events[name];
if (functionsToSpawn) {
functionsToSpawn.forEach((functionToSpawn) => {
// Only dispatch functions on events when they're allowed to be invoked in this environment
if (plug.canInvoke(functionToSpawn)) {
promises.push(plug.invoke(functionToSpawn, [data]));
}
});
}
}
return Promise.all(promises);
}
apply(system: System<EventHook>): void {
this.system = system;
system.on({
plugLoaded: (name, plug) => {},
});
}
validateManifest(manifest: Manifest<EventHook>): string[] {
return [];
}
}

View File

@ -7,16 +7,28 @@ export class Plug<HookT> {
sandbox: Sandbox;
public manifest?: Manifest<HookT>;
readonly runtimeEnv: RuntimeEnvironment;
grantedPermissions: string[] = [];
name: string;
constructor(system: System<HookT>, name: string, sandbox: Sandbox) {
constructor(
system: System<HookT>,
name: string,
sandboxFactory: (plug: Plug<HookT>) => Sandbox
) {
this.system = system;
this.sandbox = sandbox;
this.name = name;
this.sandbox = sandboxFactory(this);
this.runtimeEnv = system.runtimeEnv;
}
async load(manifest: Manifest<HookT>) {
this.manifest = manifest;
await this.dispatchEvent("load");
// TODO: These need to be explicitly granted, not just taken
this.grantedPermissions = manifest.requiredPermissions || [];
}
syscall(name: string, args: any[]): Promise<any> {
return this.system.syscallWithContext({ plug: this }, name, args);
}
canInvoke(name: string) {
@ -46,27 +58,6 @@ export class Plug<HookT> {
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();
}

View File

@ -20,9 +20,7 @@ export class DiskPlugLoader<HookT> {
watcher() {
safeRun(async () => {
for await (const { filename, eventType } of watch(this.plugPath, {
recursive: true,
})) {
for await (const { filename, eventType } of watch(this.plugPath)) {
if (!filename.endsWith(".plug.json")) {
return;
}
@ -50,7 +48,7 @@ export class DiskPlugLoader<HookT> {
console.log("Now loading plug", plugName);
try {
const plugDef = JSON.parse(plug);
await this.system.load(plugName, plugDef, createSandbox(this.system));
await this.system.load(plugName, plugDef, createSandbox);
return plugDef;
} catch (e) {
console.error("Could not parse plugin file", e);

View File

@ -4,17 +4,28 @@ import { System } from "./system";
test("Run a Node sandbox", async () => {
let system = new System("server");
system.registerSyscalls({
addNumbers: (a, b) => {
system.registerSyscalls("", [], {
addNumbers: (ctx, a, b) => {
return a + b;
},
failingSyscall: () => {
throw new Error("#fail");
},
});
system.registerSyscalls("", ["restricted"], {
restrictedSyscall: () => {
return "restricted";
},
});
system.registerSyscalls("", ["dangerous"], {
dangerousSyscall: () => {
return "yay";
},
});
let plug = await system.load(
"test",
{
requiredPermissions: ["dangerous"],
functions: {
addTen: {
code: `(() => {
@ -52,12 +63,30 @@ test("Run a Node sandbox", async () => {
};
})()`,
},
restrictedTest: {
code: `(() => {
return {
default: async () => {
await self.syscall("restrictedSyscall");
}
};
})()`,
},
dangerousTest: {
code: `(() => {
return {
default: async () => {
return await self.syscall("dangerousSyscall");
}
};
})()`,
},
},
hooks: {
events: {},
},
},
createSandbox(system)
createSandbox
);
expect(await plug.invoke("addTen", [10])).toBe(20);
for (let i = 0; i < 100; i++) {
@ -75,5 +104,15 @@ test("Run a Node sandbox", async () => {
} catch (e: any) {
expect(e.message).toBe("#fail");
}
try {
await plug.invoke("restrictedTest", []);
expect(true).toBe(false);
} catch (e: any) {
expect(e.message).toBe(
"Missing permission 'restricted' for syscall restrictedSyscall"
);
}
expect(await plug.invoke("dangerousTest", [])).toBe("yay");
await system.unloadAll();
});

View File

@ -1,9 +1,11 @@
import { System } from "./system";
import {
ControllerMessage,
WorkerLike,
WorkerMessage,
} from "./environment/worker";
import { Plug } from "./plug";
export type SandboxFactory<HookT> = (plug: Plug<HookT>) => Sandbox;
export class Sandbox {
protected worker: WorkerLike;
@ -14,12 +16,12 @@ export class Sandbox {
{ resolve: (result: any) => void; reject: (e: any) => void }
>();
protected loadedFunctions = new Set<string>();
protected system: System<any>;
protected plug: Plug<any>;
constructor(system: System<any>, worker: WorkerLike) {
constructor(plug: Plug<any>, worker: WorkerLike) {
worker.onMessage = this.onMessage.bind(this);
this.worker = worker;
this.system = system;
this.plug = plug;
}
isLoaded(name: string) {
@ -48,7 +50,7 @@ export class Sandbox {
break;
case "syscall":
try {
let result = await this.system.syscall(data.name!, data.args!);
let result = await this.plug.syscall(data.name!, data.args!);
this.worker.postMessage({
type: "syscall-response",

View File

@ -0,0 +1,15 @@
import fetch, { RequestInfo, RequestInit } from "node-fetch";
import { SysCallMapping } from "../system";
export function fetchSyscalls(): SysCallMapping {
return {
async fetchJson(ctx, url: RequestInfo, init: RequestInit) {
let resp = await fetch(url, init);
return resp.json();
},
async fetchText(ctx, url: RequestInfo, init: RequestInit) {
let resp = await fetch(url, init);
return resp.text();
},
};
}

View File

@ -1,11 +1,16 @@
import { promisify } from "util";
import { execFile } from "child_process";
import type { SysCallMapping } from "../system";
const execFilePromise = promisify(execFile);
export default function (cwd: string) {
export default function (cwd: string): SysCallMapping {
return {
"shell.run": async (cmd: string, args: string[]) => {
run: async (
ctx,
cmd: string,
args: string[]
): Promise<{ stdout: string; stderr: string }> => {
let { stdout, stderr } = await execFilePromise(cmd, args, {
cwd: cwd,
});

View File

@ -0,0 +1,50 @@
import { createSandbox } from "../environment/node_sandbox";
import { expect, test } from "@jest/globals";
import { System } from "../system";
import { storeSyscalls } from "./store.dexie_browser";
// For testing in node.js
require("fake-indexeddb/auto");
test("Test store", async () => {
let system = new System("server");
system.registerSyscalls("store", [], storeSyscalls("test", "test"));
let plug = await system.load(
"test",
{
hooks: {},
functions: {
test1: {
code: `(() => {
return {
default: async () => {
await self.syscall("store.set", "name", "Pete");
return await self.syscall("store.get", "name");
}
};
})()`,
},
test2: {
code: `(() => {
return {
default: async () => {
await self.syscall("store.set", "page1:bl:page2:10", {title: "Something", meta: 20});
await self.syscall("store.batchSet", [
{key: "page2:bl:page3", value: {title: "Something2", meta: 10}},
{key: "page2:bl:page4", value: {title: "Something3", meta: 10}},
]);
return await self.syscall("store.queryPrefix", "page2:");
}
};
})()`,
},
},
},
createSandbox
);
expect(await plug.invoke("test1", [])).toBe("Pete");
let queryResults = await plug.invoke("test2", []);
expect(queryResults.length).toBe(2);
expect(queryResults[0].value.meta).toBe(10);
await system.unloadAll();
});

View File

@ -0,0 +1,66 @@
import Dexie from "dexie";
import { SysCallMapping } from "../system";
export type KV = {
key: string;
value: any;
};
export function storeSyscalls(
dbName: string,
tableName: string
): SysCallMapping {
const db = new Dexie(dbName);
db.version(1).stores({
test: "key",
});
const items = db.table(tableName);
return {
async delete(ctx, key: string) {
await items.delete(key);
},
async deletePrefix(ctx, prefix: string) {
await items.where("key").startsWith(prefix).delete();
},
async deleteAll() {
await items.clear();
},
async set(ctx, key: string, value: any) {
await items.put({
key,
value,
});
},
async batchSet(ctx, kvs: KV[]) {
await items.bulkPut(
kvs.map(({ key, value }) => ({
key,
value,
}))
);
},
async get(ctx, key: string): Promise<any | null> {
let result = await items.get({
key,
});
return result ? result.value : null;
},
async queryPrefix(
ctx,
keyPrefix: string
): Promise<{ key: string; value: any }[]> {
let results = await items.where("key").startsWith(keyPrefix).toArray();
return results.map((result) => ({
key: result.key,
value: result.value,
}));
},
};
}

View File

@ -0,0 +1,50 @@
import { createSandbox } from "../environment/node_sandbox";
import { expect, test } from "@jest/globals";
import { System } from "../system";
import {
ensureTable,
storeReadSyscalls,
storeWriteSyscalls,
} from "./store.knex_node";
import knex from "knex";
import fs from "fs/promises";
test("Test store", async () => {
const db = knex({
client: "better-sqlite3",
connection: {
filename: "test.db",
},
useNullAsDefault: true,
});
await ensureTable(db, "test_table");
let system = new System("server");
system.registerSyscalls(
"store",
[],
storeWriteSyscalls(db, "test_table"),
storeReadSyscalls(db, "test_table")
);
let plug = await system.load(
"test",
{
hooks: {},
functions: {
test1: {
code: `(() => {
return {
default: async () => {
await self.syscall("store.set", "name", "Pete");
return await self.syscall("store.get", "name");
}
};
})()`,
},
},
},
createSandbox
);
expect(await plug.invoke("test1", [])).toBe("Pete");
await system.unloadAll();
await fs.unlink("test.db");
});

View File

@ -0,0 +1,84 @@
import { Knex } from "knex";
import { SysCallMapping } from "../system";
type Item = {
page: string;
key: string;
value: any;
};
export type KV = {
key: string;
value: any;
};
export async function ensureTable(db: Knex<any, unknown>, tableName: string) {
if (!(await db.schema.hasTable(tableName))) {
await db.schema.createTable(tableName, (table) => {
table.string("key");
table.text("value");
table.primary(["key"]);
});
console.log(`Created table ${tableName}`);
}
}
export function storeWriteSyscalls(
db: Knex<any, unknown>,
tableName: string
): SysCallMapping {
const apiObj: SysCallMapping = {
delete: async (ctx, page: string, key: string) => {
await db<Item>(tableName).where({ page, key }).del();
},
deletePrefix: async (ctx, prefix: string) => {
return db<Item>(tableName).andWhereLike("key", `${prefix}%`).del();
},
deleteAll: async (ctx) => {
await db<Item>(tableName).del();
},
set: async (ctx, key: string, value: any) => {
let changed = await db<Item>(tableName)
.where({ key })
.update("value", JSON.stringify(value));
if (changed === 0) {
await db<Item>(tableName).insert({
key,
value: JSON.stringify(value),
});
}
},
batchSet: async (ctx, kvs: KV[]) => {
for (let { key, value } of kvs) {
await apiObj["store.set"](ctx, key, value);
}
},
};
return apiObj;
}
export function storeReadSyscalls(
db: Knex<any, unknown>,
tableName: string
): SysCallMapping {
return {
get: async (ctx, key: string): Promise<any | null> => {
let result = await db<Item>(tableName).where({ key }).select("value");
if (result.length) {
return JSON.parse(result[0].value);
} else {
return null;
}
},
queryPrefix: async (ctx, prefix: string) => {
return (
await db<Item>(tableName)
.andWhereLike("key", `${prefix}%`)
.select("key", "value")
).map(({ key, value }) => ({
key,
value: JSON.parse(value),
}));
},
};
}

View File

@ -0,0 +1,16 @@
import { SysCallMapping } from "../system";
export function transportSyscalls(
names: string[],
transportCall: (name: string, ...args: any[]) => Promise<any>
): SysCallMapping {
let syscalls: SysCallMapping = {};
for (let name of names) {
syscalls[name] = (ctx, ...args: any[]) => {
return transportCall(name, ...args);
};
}
return syscalls;
}

View File

@ -1,10 +1,10 @@
import { Feature, Manifest, RuntimeEnvironment } from "./types";
import { EventEmitter } from "../common/event";
import { Sandbox } from "./sandbox";
import { SandboxFactory } from "./sandbox";
import { Plug } from "./plug";
export interface SysCallMapping {
[key: string]: (...args: any) => Promise<any> | any;
[key: string]: (ctx: SyscallContext, ...args: any) => Promise<any> | any;
}
export type SystemJSON<HookT> = { [key: string]: Manifest<HookT> };
@ -14,9 +14,23 @@ export type SystemEvents<HookT> = {
plugUnloaded: (name: string, plug: Plug<HookT>) => void;
};
type SyscallContext = {
plug: Plug<any> | null;
};
type SyscallSignature = (
ctx: SyscallContext,
...args: any[]
) => Promise<any> | any;
type Syscall = {
requiredPermissions: string[];
callback: SyscallSignature;
};
export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
protected plugs = new Map<string, Plug<HookT>>();
registeredSyscalls: SysCallMapping = {};
protected registeredSyscalls = new Map<string, Syscall>();
protected enabledFeatures = new Set<Feature<HookT>>();
readonly runtimeEnv: RuntimeEnvironment;
@ -31,29 +45,46 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
feature.apply(this);
}
registerSyscalls(...registrationObjects: SysCallMapping[]) {
registerSyscalls(
namespace: string,
requiredCapabilities: string[],
...registrationObjects: SysCallMapping[]
) {
for (const registrationObject of registrationObjects) {
for (let [name, def] of Object.entries(registrationObject)) {
this.registeredSyscalls[name] = def;
for (let [name, callback] of Object.entries(registrationObject)) {
const callName = namespace ? `${namespace}.${name}` : name;
this.registeredSyscalls.set(callName, {
requiredPermissions: requiredCapabilities,
callback,
});
}
}
}
async syscall(name: string, args: any[]): Promise<any> {
const callback = this.registeredSyscalls[name];
if (!name) {
async syscallWithContext(
ctx: SyscallContext,
name: string,
args: any[]
): Promise<any> {
const syscall = this.registeredSyscalls.get(name);
if (!syscall) {
throw Error(`Unregistered syscall ${name}`);
}
if (!callback) {
throw Error(`Registered but not implemented syscall ${name}`);
for (const permission of syscall.requiredPermissions) {
if (!ctx.plug) {
throw Error(`Syscall ${name} requires permission and no plug is set`);
}
if (!ctx.plug.grantedPermissions.includes(permission)) {
throw Error(`Missing permission '${permission}' for syscall ${name}`);
}
}
return Promise.resolve(callback(...args));
return Promise.resolve(syscall.callback(ctx, ...args));
}
async load(
name: string,
manifest: Manifest<HookT>,
sandbox: Sandbox
sandboxFactory: SandboxFactory<HookT>
): Promise<Plug<HookT>> {
if (this.plugs.has(name)) {
await this.unload(name);
@ -67,7 +98,7 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
throw new Error(`Invalid manifest: ${errors.join(", ")}`);
}
// Ok, let's load this thing!
const plug = new Plug(this, name, sandbox);
const plug = new Plug(this, name, sandboxFactory);
await plug.load(manifest);
this.plugs.set(name, plug);
this.emit("plugLoaded", name, plug);
@ -84,16 +115,6 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
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;
}
@ -111,12 +132,12 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
async replaceAllFromJSON(
json: SystemJSON<HookT>,
sandboxFactory: () => Sandbox
sandboxFactory: SandboxFactory<HookT>
) {
await this.unloadAll();
for (let [name, manifest] of Object.entries(json)) {
console.log("Loading plug", name);
await this.load(name, manifest, sandboxFactory());
await this.load(name, manifest, sandboxFactory);
}
}

View File

@ -1,7 +1,8 @@
import { System } from "./system";
export interface Manifest<HookT> {
hooks: HookT & EventHook;
requiredPermissions?: string[];
hooks: HookT;
functions: {
[key: string]: FunctionDef;
};
@ -15,10 +16,6 @@ export interface FunctionDef {
export type RuntimeEnvironment = "client" | "server";
export type EventHook = {
events?: { [key: string]: string[] };
};
export interface Feature<HookT> {
validateManifest(manifest: Manifest<HookT>): string[];

View File

@ -45,12 +45,6 @@
"path": "/",
"handler": "endpointTest"
}
],
"crons": [
{
"cron": "*/15 * * * *",
"handler": "gitSnapshot"
}
]
},
"functions": {
@ -96,10 +90,6 @@
"welcome": {
"path": "./server.ts:welcome",
"env": "server"
},
"gitSnapshot": {
"path": "./git.ts:commit",
"env": "server"
}
}
}

View File

@ -1,4 +1,4 @@
import { syscall } from "./lib/syscall";
import { syscall } from "../lib/syscall";
export async function insertToday() {
console.log("Inserting date");

View File

@ -1,12 +0,0 @@
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!");
}

View File

@ -1,4 +1,4 @@
import { syscall } from "./lib/syscall";
import { syscall } from "../lib/syscall";
export async function toggleH1() {
await togglePrefix("# ");

View File

@ -1,5 +1,5 @@
import { ClickEvent } from "../../webapp/src/app_event";
import { syscall } from "./lib/syscall";
import { ClickEvent } from "../../webapp/app_event";
import { syscall } from "../lib/syscall";
async function navigate(syntaxNode: any) {
if (!syntaxNode) {
@ -8,7 +8,6 @@ async function navigate(syntaxNode: any) {
console.log("Attempting to navigate based on syntax node", syntaxNode);
switch (syntaxNode.name) {
case "WikiLinkPage":
case "AtMention":
await syscall("editor.navigate", syntaxNode.text);
break;
case "URL":
@ -36,31 +35,16 @@ export async function clickNavigate(event: ClickEvent) {
}
export async function pageComplete() {
let prefix = await syscall(
"editor.matchBefore",
"(\\[\\[[\\w\\s]*|@[\\w\\.]*)"
);
let prefix = await syscall("editor.matchBefore", "\\[\\[[\\w\\s]*");
if (!prefix) {
return null;
}
let allPages = await syscall("space.listPages");
if (prefix.text[0] === "@") {
return {
from: prefix.from,
options: allPages
.filter((page: any) => page.name.startsWith(prefix.text))
.map((pageMeta: any) => ({
label: pageMeta.name,
type: "page",
})),
};
} else {
return {
from: prefix.from + 2,
options: allPages.map((pageMeta: any) => ({
label: pageMeta.name,
type: "page",
})),
};
}
return {
from: prefix.from + 2,
options: allPages.map((pageMeta: any) => ({
label: pageMeta.name,
type: "page",
})),
};
}

View File

@ -1,13 +1,13 @@
import { IndexEvent } from "../../webapp/app_event";
import { pageLinkRegex } from "../../webapp/constant";
import { syscall } from "./lib/syscall";
import { syscall } from "../lib/syscall";
const wikilinkRegex = new RegExp(pageLinkRegex, "g");
const atMentionRegex = /(@[A-Za-z\.]+)/g;
export async function indexLinks({ name, text }: IndexEvent) {
let backLinks: { key: string; value: string }[] = [];
// [[Style Links]]
for (let match of text.matchAll(wikilinkRegex)) {
let toPage = match[1];
let pos = match.index!;
@ -16,15 +16,6 @@ export async function indexLinks({ name, text }: IndexEvent) {
value: name,
});
}
// @links
for (let match of text.matchAll(atMentionRegex)) {
let toPage = match[1];
let pos = match.index!;
backLinks.push({
key: `pl:${toPage}:${pos}`,
value: name,
});
}
console.log("Found", backLinks.length, "wiki link(s)");
// throw Error("Boom");
await syscall("indexer.batchSet", name, backLinks);

View File

@ -13,4 +13,5 @@ export function endpointTest(req: EndpointRequest): EndpointResponse {
export function welcome() {
console.log("Hello world!");
return "hi";
}

View File

@ -1,5 +1,5 @@
import { ClickEvent } from "../../webapp/src/app_event";
import { syscall } from "./lib/syscall";
import { syscall } from "../lib/syscall";
export async function taskToggle(event: ClickEvent) {
let syntaxNode = await syscall("editor.getSyntaxNodeAtPos", event.pos);

View File

@ -1,4 +1,4 @@
import { syscall } from "./lib/syscall";
import { syscall } from "../lib/syscall";
function countWords(str: string): number {
var matches = str.match(/[\w\d\'\'-]+/gi);

37
plugs/git/git.plug.json Normal file
View File

@ -0,0 +1,37 @@
{
"requiredPermissions": ["shell"],
"hooks": {
"commands": {
"Git: Snapshot": {
"invoke": "snapshotCommand"
},
"Git: Sync": {
"invoke": "syncCommand"
}
},
"crons": [
{
"cron": "*/15 * * * *",
"handler": "commit"
}
]
},
"functions": {
"snapshotCommand": {
"path": "./git.ts:snapshotCommand",
"env": "client"
},
"syncCommand": {
"path": "./git.ts:syncCommand",
"env": "client"
},
"commit": {
"path": "./git.ts:commit",
"env": "server"
},
"sync": {
"path": "./git.ts:sync",
"env": "server"
}
}
}

42
plugs/git/git.ts Normal file
View File

@ -0,0 +1,42 @@
import { syscall } from "../lib/syscall";
export async function commit(message?: string) {
if (!message) {
message = "Snapshot";
}
console.log(
"Snapshotting the current space to git with commit message",
message
);
await syscall("shell.run", "git", ["add", "./*.md"]);
try {
await syscall("shell.run", "git", ["commit", "-a", "-m", message]);
} catch (e) {
// We can ignore, this happens when there's no changes to commit
}
console.log("Done!");
}
export async function snapshotCommand() {
let revName = await syscall("editor.prompt", `Revision name:`);
if (!revName) {
revName = "Snapshot";
}
console.log("Revision name", revName);
await syscall("system.invokeFunctionOnServer", "commit", revName);
}
export async function syncCommand() {
await syscall("system.invokeFunctionOnServer", "sync");
}
export async function sync() {
console.log("Going to sync with git");
console.log("First locally committing everything");
await commit();
console.log("Then pulling from remote");
await syscall("shell.run", "git", ["pull"]);
console.log("And then pushing to remote");
await syscall("shell.run", "git", ["push"]);
console.log("Done!");
}

View File

@ -46,7 +46,7 @@ export class SocketServer {
public async init() {
const indexApi = new IndexApi(this.rootPath);
await this.registerApi("index", indexApi);
this.system.registerSyscalls(pageIndexSyscalls(indexApi.db));
this.system.registerSyscalls("indexer", [], pageIndexSyscalls(indexApi.db));
await this.registerApi(
"page",
new PageApi(
@ -118,6 +118,24 @@ export class SocketServer {
});
}
onCall(
"invokeFunction",
(plugName: string, name: string, ...args: any[]): Promise<any> => {
let plug = this.system.loadedPlugs.get(plugName);
if (!plug) {
throw new Error(`Plug ${plugName} not loaded`);
}
console.log(
"Invoking function",
name,
"for plug",
plugName,
"as requested over socket"
);
return plug.invoke(name, args);
}
);
console.log("Sending the sytem to the client");
socket.emit("loadSystem", this.system.toJSON());
});

View File

@ -36,12 +36,13 @@ export class IndexApi implements ApiProvider {
api() {
const syscalls = pageIndexSyscalls(this.db);
const nullContext = { plug: null };
return {
clearPageIndexForPage: async (
clientConn: ClientConnection,
page: string
) => {
return syscalls["indexer.clearPageIndexForPage"](page);
return syscalls.clearPageIndexForPage(nullContext, page);
},
set: async (
clientConn: ClientConnection,
@ -49,41 +50,41 @@ export class IndexApi implements ApiProvider {
key: string,
value: any
) => {
return syscalls["indexer.set"](page, key, value);
return syscalls.set(nullContext, page, key, value);
},
get: async (clientConn: ClientConnection, page: string, key: string) => {
return syscalls["indexer.get"](page, key);
return syscalls.get(nullContext, page, key);
},
delete: async (
clientConn: ClientConnection,
page: string,
key: string
) => {
return syscalls["indexer.delete"](page, key);
return syscalls.delete(nullContext, page, key);
},
scanPrefixForPage: async (
clientConn: ClientConnection,
page: string,
prefix: string
) => {
return syscalls["indexer.scanPrefixForPage"](page, prefix);
return syscalls.scanPrefixForPage(nullContext, page, prefix);
},
scanPrefixGlobal: async (
clientConn: ClientConnection,
prefix: string
) => {
return syscalls["indexer.scanPrefixGlobal"](prefix);
return syscalls.scanPrefixGlobal(nullContext, prefix);
},
deletePrefixForPage: async (
clientConn: ClientConnection,
page: string,
prefix: string
) => {
return syscalls["indexer.deletePrefixForPage"](page, prefix);
return syscalls.deletePrefixForPage(nullContext, page, prefix);
},
clearPageIndex: async (clientConn: ClientConnection) => {
return syscalls["indexer.clearPageIndex"]();
return syscalls.clearPageIndex(nullContext);
},
};
}

View File

@ -11,6 +11,7 @@ import { stat } from "fs/promises";
import { Cursor, cursorEffect } from "../webapp/cursorEffect";
import { SilverBulletHooks } from "../common/manifest";
import { System } from "../plugbox/system";
import { EventFeature } from "../plugbox/feature/event";
export class PageApi implements ApiProvider {
openPages: Map<string, Page>;
@ -18,6 +19,7 @@ export class PageApi implements ApiProvider {
rootPath: string;
connectedSockets: Set<Socket>;
private system: System<SilverBulletHooks>;
private eventFeature: EventFeature;
constructor(
rootPath: string,
@ -30,6 +32,8 @@ export class PageApi implements ApiProvider {
this.openPages = openPages;
this.connectedSockets = connectedSockets;
this.system = system;
this.eventFeature = new EventFeature();
system.addFeature(this.eventFeature);
}
async init(): Promise<void> {
@ -222,7 +226,7 @@ export class PageApi implements ApiProvider {
);
await this.flushPageToDisk(pageName, page);
await this.system.dispatchEvent("page:index", {
await this.eventFeature.dispatchEvent("page:index", {
name: pageName,
text: page.text.sliceString(0),
});

View File

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

View File

@ -1,4 +1,5 @@
import { Knex } from "knex";
import { SysCallMapping } from "../../plugbox/system";
type IndexItem = {
page: string;
@ -11,12 +12,12 @@ export type KV = {
value: any;
};
export default function (db: Knex) {
const apiObj = {
"indexer.clearPageIndexForPage": async (page: string) => {
export default function (db: Knex): SysCallMapping {
const apiObj: SysCallMapping = {
clearPageIndexForPage: async (ctx, page: string) => {
await db<IndexItem>("page_index").where({ page }).del();
},
"indexer.set": async (page: string, key: string, value: any) => {
set: async (ctx, page: string, key: string, value: any) => {
let changed = await db<IndexItem>("page_index")
.where({ page, key })
.update("value", JSON.stringify(value));
@ -28,12 +29,12 @@ export default function (db: Knex) {
});
}
},
"indexer.batchSet": async (page: string, kvs: KV[]) => {
batchSet: async (ctx, page: string, kvs: KV[]) => {
for (let { key, value } of kvs) {
await apiObj["indexer.set"](page, key, value);
await apiObj.set(ctx, page, key, value);
}
},
"indexer.get": async (page: string, key: string) => {
get: async (ctx, page: string, key: string) => {
let result = await db<IndexItem>("page_index")
.where({ page, key })
.select("value");
@ -43,10 +44,10 @@ export default function (db: Knex) {
return null;
}
},
"indexer.delete": async (page: string, key: string) => {
delete: async (ctx, page: string, key: string) => {
await db<IndexItem>("page_index").where({ page, key }).del();
},
"indexer.scanPrefixForPage": async (page: string, prefix: string) => {
scanPrefixForPage: async (ctx, page: string, prefix: string) => {
return (
await db<IndexItem>("page_index")
.where({ page })
@ -58,7 +59,7 @@ export default function (db: Knex) {
value: JSON.parse(value),
}));
},
"indexer.scanPrefixGlobal": async (prefix: string) => {
scanPrefixGlobal: async (ctx, prefix: string) => {
return (
await db<IndexItem>("page_index")
.andWhereLike("key", `${prefix}%`)
@ -69,13 +70,13 @@ export default function (db: Knex) {
value: JSON.parse(value),
}));
},
"indexer.deletePrefixForPage": async (page: string, prefix: string) => {
deletePrefixForPage: async (ctx, page: string, prefix: string) => {
return db<IndexItem>("page_index")
.where({ page })
.andWhereLike("key", `${prefix}%`)
.del();
},
"indexer.clearPageIndex": async () => {
clearPageIndex: async () => {
return db<IndexItem>("page_index").del();
},
};

View File

@ -1,9 +1,5 @@
{
"include": [
"webapp/**/*",
"server/**/*",
"plugbox/**/*"
],
"include": ["webapp/**/*", "server/**/*", "plugbox/**/*", "plugs/**/*"],
"compilerOptions": {
"target": "esnext",
"strict": true,
@ -12,6 +8,7 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"downlevelIteration": true
}
}

View File

@ -179,7 +179,7 @@ export function collabExtension(
if (this.pushing || !updates.length) return;
this.pushing = true;
let version = getSyncedVersion(this.view.state);
console.log("Updates", updates, "to apply to version", version);
// console.log("Updates", updates, "to apply to version", version);
let success = await callbacks.pushUpdates(pageName, version, updates);
this.pushing = false;
@ -201,7 +201,6 @@ export function collabExtension(
// Regardless of whether the push failed or new updates came in
// while it was running, try again if there's updates remaining
if (!this.done && sendableUpdates(this.view.state).length) {
// setTimeout(() => this.push(), 100);
this.throttledPush();
}
}

View File

@ -1,7 +1,6 @@
import { AppViewState, PageMeta } from "../types";
import { Notification } from "../types";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faFileLines } from "@fortawesome/free-solid-svg-icons";
import { Notification } from "../types";
function prettyName(s: string | undefined): string {
if (!s) {
@ -28,11 +27,13 @@ export function TopBar({
<FontAwesomeIcon icon={faFileLines} />
</span>
<span className="current-page">{prettyName(pageName)}</span>
<div className="status">
{notifications.map((notification) => (
<div key={notification.id}>{notification.message}</div>
))}
</div>
{notifications.length > 0 && (
<div className="status">
{notifications.map((notification) => (
<div key={notification.id}>{notification.message}</div>
))}
</div>
)}
</div>
</div>
);

View File

@ -37,9 +37,9 @@ import reducer from "./reducer";
import { smartQuoteKeymap } from "./smart_quotes";
import { Space } from "./space";
import customMarkdownStyle from "./style";
import editorSyscalls from "./syscalls/editor.browser";
import indexerSyscalls from "./syscalls/indexer.native";
import spaceSyscalls from "./syscalls/space.native";
import editorSyscalls from "./syscalls/editor";
import indexerSyscalls from "./syscalls/indexer";
import spaceSyscalls from "./syscalls/space";
import {
Action,
AppCommand,
@ -50,6 +50,8 @@ import {
import { SilverBulletHooks } from "../common/manifest";
import { safeRun } from "./util";
import { System } from "../plugbox/system";
import { EventFeature } from "../plugbox/feature/event";
import { systemSyscalls } from "./syscalls/system";
class PageState {
scrollTop: number;
@ -71,11 +73,16 @@ export class Editor implements AppEventDispatcher {
space: Space;
navigationResolve?: (val: undefined) => void;
pageNavigator: IPageNavigator;
private eventFeature: EventFeature;
constructor(space: Space, parent: Element) {
this.space = space;
this.viewState = initialViewState;
this.viewDispatch = () => {};
this.eventFeature = new EventFeature();
this.system.addFeature(this.eventFeature);
this.render(parent);
this.editorView = new EditorView({
state: this.createEditorState(
@ -86,11 +93,10 @@ export class Editor implements AppEventDispatcher {
});
this.pageNavigator = new PathPageNavigator();
this.system.registerSyscalls(
editorSyscalls(this),
spaceSyscalls(this),
indexerSyscalls(this.space)
);
this.system.registerSyscalls("editor", [], editorSyscalls(this));
this.system.registerSyscalls("space", [], spaceSyscalls(this));
this.system.registerSyscalls("indexer", [], indexerSyscalls(this.space));
this.system.registerSyscalls("system", [], systemSyscalls(this.space));
}
async init() {
@ -129,20 +135,14 @@ export class Editor implements AppEventDispatcher {
},
loadSystem: (systemJSON) => {
safeRun(async () => {
await this.system.replaceAllFromJSON(systemJSON, () =>
createIFrameSandbox(this.system)
);
await this.system.replaceAllFromJSON(systemJSON, createIFrameSandbox);
this.buildAllCommands();
});
},
plugLoaded: (plugName, plug) => {
safeRun(async () => {
console.log("Plug load", plugName);
await this.system.load(
plugName,
plug,
createIFrameSandbox(this.system)
);
await this.system.load(plugName, plug, createIFrameSandbox);
this.buildAllCommands();
});
},
@ -199,7 +199,7 @@ export class Editor implements AppEventDispatcher {
}
async dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]> {
return this.system.dispatchEvent(name, data);
return this.eventFeature.dispatchEvent(name, data);
}
get currentPage(): string | undefined {

View File

@ -80,7 +80,7 @@ export class Space extends EventEmitter<SpaceEvents> {
});
}
private wsCall(eventName: string, ...args: any[]): Promise<any> {
public wsCall(eventName: string, ...args: any[]): Promise<any> {
return new Promise((resolve, reject) => {
this.reqId++;
this.socket!.once(`${eventName}Resp${this.reqId}`, (err, result) => {
@ -160,40 +160,4 @@ export class Space extends EventEmitter<SpaceEvents> {
async getPageMeta(name: string): Promise<PageMeta> {
return this.wsCall("page.getPageMeta", name);
}
async indexSet(pageName: string, key: string, value: any) {
await this.wsCall("index.set", pageName, key, value);
}
async indexBatchSet(pageName: string, kvs: KV[]) {
// TODO: Optimize with batch call
for (let { key, value } of kvs) {
await this.indexSet(pageName, key, value);
}
}
async indexGet(pageName: string, key: string): Promise<any | null> {
return await this.wsCall("index.get", pageName, key);
}
async indexScanPrefixForPage(
pageName: string,
keyPrefix: string
): Promise<{ key: string; value: any }[]> {
return await this.wsCall("index.scanPrefixForPage", pageName, keyPrefix);
}
async indexScanPrefixGlobal(
keyPrefix: string
): Promise<{ key: string; value: any }[]> {
return await this.wsCall("index.scanPrefixGlobal", keyPrefix);
}
async indexDeletePrefixForPage(pageName: string, keyPrefix: string) {
await this.wsCall("index.deletePrefixForPage", keyPrefix);
}
async indexDelete(pageName: string, key: string) {
await this.wsCall("index.delete", pageName, key);
}
}

View File

@ -129,7 +129,6 @@
.mention {
color: #0330cb;
text-decoration: underline;
}
.tag {

View File

@ -1,8 +0,0 @@
export default {
"db.put": (key: string, value: any) => {
localStorage.setItem(key, value);
},
"db.get": (key: string) => {
return localStorage.getItem(key);
},
};

View File

@ -1,7 +1,7 @@
import { Editor } from "../editor";
import { syntaxTree } from "@codemirror/language";
import { Transaction } from "@codemirror/state";
import { PageMeta } from "../types";
import { SysCallMapping } from "../../plugbox/system";
type SyntaxNode = {
name: string;
@ -26,23 +26,23 @@ function ensureAnchor(expr: any, start: boolean) {
);
}
export default (editor: Editor) => ({
"editor.getCurrentPage": (): string => {
export default (editor: Editor): SysCallMapping => ({
getCurrentPage: (): string => {
return editor.currentPage!;
},
"editor.getText": () => {
getText: () => {
return editor.editorView?.state.sliceDoc();
},
"editor.getCursor": (): number => {
getCursor: (): number => {
return editor.editorView!.state.selection.main.from;
},
"editor.navigate": async (name: string) => {
navigate: async (ctx, name: string) => {
await editor.navigate(name);
},
"editor.openUrl": async (url: string) => {
openUrl: async (ctx, url: string) => {
window.open(url, "_blank")!.focus();
},
"editor.insertAtPos": (text: string, pos: number) => {
insertAtPos: (ctx, text: string, pos: number) => {
editor.editorView!.dispatch({
changes: {
insert: text,
@ -50,7 +50,7 @@ export default (editor: Editor) => ({
},
});
},
"editor.replaceRange": (from: number, to: number, text: string) => {
replaceRange: (ctx, from: number, to: number, text: string) => {
editor.editorView!.dispatch({
changes: {
insert: text,
@ -59,14 +59,14 @@ export default (editor: Editor) => ({
},
});
},
"editor.moveCursor": (pos: number) => {
moveCursor: (ctx, pos: number) => {
editor.editorView!.dispatch({
selection: {
anchor: pos,
},
});
},
"editor.insertAtCursor": (text: string) => {
insertAtCursor: (ctx, text: string) => {
let editorView = editor.editorView!;
let from = editorView.state.selection.main.from;
editorView.dispatch({
@ -79,7 +79,7 @@ export default (editor: Editor) => ({
},
});
},
"editor.getSyntaxNodeUnderCursor": (): SyntaxNode | undefined => {
getSyntaxNodeUnderCursor: (): SyntaxNode | undefined => {
const editorState = editor.editorView!.state;
let selection = editorState.selection.main;
if (selection.empty) {
@ -94,7 +94,8 @@ export default (editor: Editor) => ({
}
}
},
"editor.matchBefore": (
matchBefore: (
ctx,
regexp: string
): { from: number; to: number; text: string } | null => {
const editorState = editor.editorView!.state;
@ -112,7 +113,7 @@ export default (editor: Editor) => ({
}
return null;
},
"editor.getSyntaxNodeAtPos": (pos: number): SyntaxNode | undefined => {
getSyntaxNodeAtPos: (ctx, pos: number): SyntaxNode | undefined => {
const editorState = editor.editorView!.state;
let node = syntaxTree(editorState).resolveInner(pos);
if (node) {
@ -124,10 +125,10 @@ export default (editor: Editor) => ({
};
}
},
"editor.dispatch": (change: Transaction) => {
dispatch: (ctx, change: Transaction) => {
editor.editorView!.dispatch(change);
},
"editor.prompt": (message: string, defaultValue = ""): string | null => {
prompt: (ctx, message: string, defaultValue = ""): string | null => {
return prompt(message, defaultValue);
},
});

View File

@ -1,22 +0,0 @@
import { Space, KV } from "../space";
export default (space: Space) => ({
"indexer.scanPrefixForPage": async (pageName: string, keyPrefix: string) => {
return await space.indexScanPrefixForPage(pageName, keyPrefix);
},
"indexer.scanPrefixGlobal": async (keyPrefix: string) => {
return await space.indexScanPrefixGlobal(keyPrefix);
},
"indexer.get": async (pageName: string, key: string): Promise<any> => {
return await space.indexGet(pageName, key);
},
"indexer.set": async (pageName: string, key: string, value: any) => {
await space.indexSet(pageName, key, value);
},
"indexer.batchSet": async (pageName: string, kvs: KV[]) => {
await space.indexBatchSet(pageName, kvs);
},
"indexer.delete": async (pageName: string, key: string) => {
await space.indexDelete(pageName, key);
},
});

View File

@ -0,0 +1,17 @@
import { Space } from "../space";
import { SysCallMapping } from "../../plugbox/system";
import { transportSyscalls } from "../../plugbox/syscall/transport";
export default function indexerSyscalls(space: Space): SysCallMapping {
return transportSyscalls(
[
"scanPrefixForPage",
"scanPrefixGlobal",
"get",
"set",
"batchSet",
"delete",
],
(name, ...args) => space.wsCall(`index.${name}`, ...args)
);
}

View File

@ -1,21 +1,21 @@
import { Editor } from "../editor";
import { PageMeta } from "../types";
import { SysCallMapping } from "../../plugbox/system";
export default (editor: Editor) => ({
"space.listPages": (): PageMeta[] => {
export default (editor: Editor): SysCallMapping => ({
listPages: (): PageMeta[] => {
return [...editor.viewState.allPages];
},
"space.readPage": async (
readPage: async (
ctx,
name: string
): Promise<{ text: string; meta: PageMeta }> => {
return await editor.space.readPage(name);
},
"space.writePage": async (name: string, text: string): Promise<PageMeta> => {
writePage: async (ctx, name: string, text: string): Promise<PageMeta> => {
return await editor.space.writePage(name, text);
},
"space.deletePage": async (name: string) => {
console.log("Clearing page index", name);
await editor.space.indexDeletePrefixForPage(name, "");
deletePage: async (ctx, name: string) => {
// If we're deleting the current page, navigate to the start page
if (editor.currentPage === name) {
await editor.navigate("start");

13
webapp/syscalls/system.ts Normal file
View File

@ -0,0 +1,13 @@
import { SysCallMapping } from "../../plugbox/system";
import { Space } from "../space";
export function systemSyscalls(space: Space): SysCallMapping {
return {
async invokeFunctionOnServer(ctx, name: string, ...args: any[]) {
if (!ctx.plug) {
throw Error("No plug associated with context");
}
return await space.wsCall("invokeFunction", ctx.plug.name, name, ...args);
},
};
}

View File

@ -1,20 +0,0 @@
// @ts-ignore
let frameTest = document.getElementById("main-frame");
window.addEventListener("message", async (event) => {
let messageEvent = event as MessageEvent;
let data = messageEvent.data;
if (data.type === "iframe_event") {
// @ts-ignore
window.mainPlug.dispatchEvent(data.data.event, data.data.data);
}
});
export default {
"ui.update": function (doc: any) {
// frameTest.contentWindow.postMessage({
// type: "loadContent",
// doc: doc,
// });
},
};

View File

@ -1,12 +0,0 @@
{
"include": ["src/**/*"],
"compilerOptions": {
"target": "esnext",
"strict": true,
"moduleResolution": "node",
"module": "ESNext",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"jsx": "react-jsx"
}
}

102
yarn.lock
View File

@ -1631,6 +1631,14 @@
resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.1.tgz#e01a874d4c2aa1a02ebc64cfd1cd8ebdbad7a996"
integrity sha512-BkMHHonDT8NJUE/pQ3kr5v2GLDKm5or9btLBoBx4F2MB2cuqYC748LYMDC55VlrLI5qZZv+Qgc3m4P3dBPcmeg==
"@types/node-fetch@^2.6.1":
version "2.6.1"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==
dependencies:
"@types/node" "*"
form-data "^3.0.0"
"@types/node@*", "@types/node@>=10.0.0", "@types/node@^17.0.21":
version "17.0.21"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644"
@ -1976,6 +1984,11 @@ base-x@^3.0.8:
dependencies:
safe-buffer "^5.0.1"
base64-arraybuffer-es6@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.7.0.tgz#dbe1e6c87b1bf1ca2875904461a7de40f21abc86"
integrity sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -2486,6 +2499,11 @@ cookiejar@^2.1.3:
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc"
integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ==
core-js@^3.4:
version "3.21.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94"
integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
@ -2691,6 +2709,11 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
data-uri-to-buffer@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b"
integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==
data-urls@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
@ -2863,6 +2886,13 @@ domelementtype@^2.0.1, domelementtype@^2.2.0:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
domexception@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
dependencies:
webidl-conversions "^4.0.2"
domexception@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
@ -3320,6 +3350,13 @@ express@^4.17.3:
utils-merge "1.0.1"
vary "~1.1.2"
fake-indexeddb@^3.1.7:
version "3.1.7"
resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-3.1.7.tgz#d9efbeade113c15efbe862e4598a4b0a1797ed9f"
integrity sha512-CUGeCzCOVjmeKi2C0pcvSh6NDU6uQIaS+7YyR++tO/atJJujkBYVhDvfePdz/U8bD33BMVWirsr1MKczfAqbjA==
dependencies:
realistic-structured-clone "^2.0.1"
fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
@ -3342,6 +3379,14 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
version "3.1.5"
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.1.5.tgz#0077bf5f3fcdbd9d75a0b5362f77dbb743489863"
integrity sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==
dependencies:
node-domexception "^1.0.0"
web-streams-polyfill "^3.0.3"
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@ -3398,6 +3443,13 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
formdata-polyfill@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
dependencies:
fetch-blob "^3.1.2"
formidable@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff"
@ -4914,6 +4966,20 @@ node-cron@^3.0.0:
dependencies:
moment-timezone "^0.5.31"
node-domexception@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.2.3.tgz#a03c9cc2044d21d1a021566bd52f080f333719a6"
integrity sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA==
dependencies:
data-uri-to-buffer "^4.0.0"
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-gyp-build@^4.2.3, node-gyp-build@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
@ -5757,6 +5823,16 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
realistic-structured-clone@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-2.0.4.tgz#7eb4c2319fc3cb72f4c8d3c9e888b11647894b50"
integrity sha512-lItAdBIFHUSe6fgztHPtmmWqKUgs+qhcYLi3wTRUl4OTB3Vb8aBVSjGfQZUvkmJCKoX3K9Wf7kyLp/F/208+7A==
dependencies:
core-js "^3.4"
domexception "^1.0.1"
typeson "^6.1.0"
typeson-registry "^1.0.0-alpha.20"
rechoir@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22"
@ -6493,6 +6569,20 @@ typescript@^4.6.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4"
integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==
typeson-registry@^1.0.0-alpha.20:
version "1.0.0-alpha.39"
resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.39.tgz#9e0f5aabd5eebfcffd65a796487541196f4b1211"
integrity sha512-NeGDEquhw+yfwNhguLPcZ9Oj0fzbADiX4R0WxvoY8nGhy98IbzQy1sezjoEFWOywOboj/DWehI+/aUlRVrJnnw==
dependencies:
base64-arraybuffer-es6 "^0.7.0"
typeson "^6.0.0"
whatwg-url "^8.4.0"
typeson@^6.0.0, typeson@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b"
integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA==
uglify-js@^3.15.1:
version "3.15.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.3.tgz#9aa82ca22419ba4c0137642ba0df800cb06e0471"
@ -6650,6 +6740,16 @@ weak-lru-cache@^1.2.2:
resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19"
integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==
web-streams-polyfill@^3.0.3:
version "3.2.0"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"
integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
@ -6672,7 +6772,7 @@ whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
whatwg-url@^8.0.0, whatwg-url@^8.5.0:
whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"
integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==