1
0
This commit is contained in:
Zef Hemel 2022-03-07 10:21:02 +01:00
parent b6046ca974
commit 653e77c4dd
32 changed files with 7275 additions and 324 deletions

View File

@ -0,0 +1,4 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}

14
mobile/.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# macOS
.DS_Store

47
mobile/App.tsx Normal file
View File

@ -0,0 +1,47 @@
import { StatusBar } from "expo-status-bar";
import React from "react";
import { SafeAreaView, StyleSheet, Text, View } from "react-native";
import { WebView } from "react-native-webview";
function safeRun(fn: () => Promise<void>) {
return fn().catch((e) => {
console.error(e);
});
}
export default function App() {
const html = require("./bundle.json");
let ref = React.useRef<WebView>(null);
return (
<SafeAreaView style={styles.container}>
<Text
style={{
color: "#fff",
backgroundColor: "#333",
height: 40,
}}
onPress={() => {
ref.current?.injectJavaScript('receiveMessage("Sup");');
}}
>
This is a header
</Text>
<WebView
style={styles.container}
ref={ref}
originWhitelist={["*"]}
source={{ html: html.html }}
onMessage={(event) => {
console.log("Got event", event.nativeEvent.data);
}}
/>
<StatusBar style="auto" />
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

32
mobile/app.json Normal file
View File

@ -0,0 +1,32 @@
{
"expo": {
"name": "mobile",
"slug": "mobile",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
mobile/assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
mobile/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
mobile/assets/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

6
mobile/babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};

1
mobile/bla.json Normal file
View File

@ -0,0 +1 @@
{ "html": "<h1><center>Hello world!!</center></h1>" }

6
mobile/build.py Normal file
View File

@ -0,0 +1,6 @@
import json
html = open("dist/index.html", "r").read()
f = open("bundle.json", "w")
f.write(json.dumps({"html": html}))
f.close()

1
mobile/bundle.json Normal file

File diff suppressed because one or more lines are too long

48
mobile/html/boot.ts Normal file
View File

@ -0,0 +1,48 @@
import { Editor } from "../../webapp/src/editor";
import { HttpRemoteSpace } from "../../webapp/src/space";
declare namespace window {
var ReactNativeWebView: any;
var receiveMessage: any;
}
function safeRun(fn: () => Promise<void>) {
fn().catch((e) => {
console.error(e);
});
}
window.receiveMessage = (msg: string) => {
console.log("Received message", msg);
};
// @ts-ignore
window.onerror = (msg, source, lineno, colno, error) => {
console.error("Error", msg, source, lineno, error);
};
console.log = (...args) => {
window.ReactNativeWebView.postMessage(
JSON.stringify({ type: "console.log", args: args })
);
};
console.error = (...args) => {
window.ReactNativeWebView.postMessage(
JSON.stringify({ type: "console.error", args: args })
);
};
try {
let editor = new Editor(
new HttpRemoteSpace(`http://192.168.2.22:3000/fs`, null),
document.getElementById("root")!
);
console.log("Initing editor");
safeRun(async () => {
await editor.loadPageList();
await editor.loadPlugs();
editor.focus();
console.log("Inited", editor.viewState);
});
} catch (e: any) {
console.error("Got an error", e.message);
}

19
mobile/html/index.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Page</title>
<style>
@import "../../webapp/src/styles/main.scss";
</style>
<script type="module" src="boot.ts"></script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body>
<div id="root"></div>
<script type="module">
import "./boot";
</script>
</body>
</html>

31
mobile/package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "mobile",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"expo": "~44.0.0",
"expo-status-bar": "~1.2.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-native": "0.64.3",
"react-native-fs": "^2.19.0",
"react-native-web": "0.17.1",
"react-native-webview": "11.15.0"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"@parcel/transformer-sass": "2.3.2",
"@types/react": "~17.0.21",
"@types/react-native": "~0.64.12",
"parcel": "^2.3.2",
"typescript": "~4.3.5"
},
"private": true
}

6
mobile/tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}

6892
mobile/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,22 +7,39 @@ import path from "path";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
async function compile(filePath, sourceMap) {
let tempFile = "out.js";
async function compile(filePath, functionName, debug) {
let outFile = "out.js";
let inFile = filePath;
if (functionName) {
// Generate a new file importing just this one function and exporting it
inFile = "in.js";
await writeFile(
inFile,
`import {${functionName}} from "./${filePath}";
export default ${functionName};`
);
}
// TODO: Figure out how to make source maps work correctly with eval() code
let js = await esbuild.build({
entryPoints: [filePath],
entryPoints: [inFile],
bundle: true,
format: "iife",
globalName: "mod",
platform: "neutral",
sourcemap: sourceMap ? "inline" : false,
minify: true,
outfile: tempFile,
sourcemap: false, //sourceMap ? "inline" : false,
minify: !debug,
outfile: outFile,
});
let jsCode = (await readFile(tempFile)).toString();
let jsCode = (await readFile(outFile)).toString();
jsCode = jsCode.replace(/^var mod ?= ?/, "");
await unlink(tempFile);
await unlink(outFile);
if (inFile !== filePath) {
await unlink(inFile);
}
return jsCode;
}
@ -35,13 +52,10 @@ async function bundle(manifestPath, sourceMaps) {
filePath = path.join(rootPath, def.path);
if (filePath.indexOf(":") !== -1) {
[filePath, jsFunctionName] = filePath.split(":");
} else if (!jsFunctionName) {
jsFunctionName = "default";
}
def.code = await compile(filePath, sourceMaps);
def.path = filePath;
def.functionName = jsFunctionName;
def.code = await compile(filePath, jsFunctionName, sourceMaps);
delete def.path;
}
return manifest;
}

View File

@ -0,0 +1,88 @@
declare global {
function syscall(id: string, name: string, args: any[]): Promise<any>;
}
import { safeRun } from "./util";
let func: Function | null = null;
let pendingRequests = new Map<string, (result: unknown) => void>();
self.syscall = async (id: string, name: string, args: any[]) => {
return await new Promise((resolve, reject) => {
pendingRequests.set(id, resolve);
self.postMessage({
type: "syscall",
id,
name,
args,
});
});
};
self.addEventListener("result", (event) => {
let customEvent = event as CustomEvent;
self.postMessage({
type: "result",
result: customEvent.detail,
});
});
self.addEventListener("app-error", (event) => {
let customEvent = event as CustomEvent;
self.postMessage({
type: "error",
reason: customEvent.detail,
});
});
function wrapScript(code: string): string {
return `const fn = ${code};
return fn["default"].apply(null, arguments);`;
}
self.addEventListener("message", (event) => {
safeRun(async () => {
let messageEvent = event;
let data = messageEvent.data;
switch (data.type) {
case "boot":
console.log("Booting", data.name);
func = new Function(wrapScript(data.code));
self.postMessage({
type: "inited",
});
break;
case "invoke":
if (!func) {
throw new Error("No function loaded");
}
try {
let result = await Promise.resolve(func(...(data.args || [])));
self.postMessage({
type: "result",
result: result,
});
} catch (e: any) {
self.postMessage({
type: "error",
reason: e.message,
});
throw e;
}
break;
case "syscall-response":
let id = data.id;
const lookup = pendingRequests.get(id);
if (!lookup) {
console.log(
"Current outstanding requests",
pendingRequests,
"looking up",
id
);
throw Error("Invalid request id");
}
pendingRequests.delete(id);
lookup(data.data);
}
});
});

View File

@ -13,18 +13,17 @@ export class FunctionWorker {
private invokeReject?: (reason?: any) => void;
private plug: Plug<any>;
constructor(plug: Plug<any>, pathPrefix: string, name: string) {
let worker = window.Worker;
this.worker = new worker("/function_worker.js");
constructor(plug: Plug<any>, name: string, code: string) {
// let worker = window.Worker;
this.worker = new Worker(new URL("function_worker.ts", import.meta.url), {
type: "module",
});
// console.log("Starting worker", this.worker);
this.worker.onmessage = this.onmessage.bind(this);
this.worker.postMessage({
type: "boot",
prefix: pathPrefix,
name: name,
// @ts-ignore
userAgent: navigator.userAgent,
code: code,
});
this.inited = new Promise((resolve) => {
this.initCallback = resolve;
@ -81,33 +80,31 @@ export interface PlugLoader<HookT> {
}
export class Plug<HookT> {
pathPrefix: string;
system: System<HookT>;
private runningFunctions: Map<string, FunctionWorker>;
public manifest?: Manifest<HookT>;
private name: string;
constructor(system: System<HookT>, pathPrefix: string, name: string) {
this.name = name;
this.pathPrefix = `${pathPrefix}/${name}`;
constructor(system: System<HookT>, name: string) {
this.system = system;
this.runningFunctions = new Map<string, FunctionWorker>();
}
async load(manifest: Manifest<HookT>) {
this.manifest = manifest;
await this.system.plugLoader.load(this.name, manifest);
await this.dispatchEvent("load");
}
async invoke(name: string, args: Array<any>): Promise<any> {
if (!this.runningFunctions.has(name)) {
this.runningFunctions.set(
let worker = this.runningFunctions.get(name);
if (!worker) {
worker = new FunctionWorker(
this,
name,
new FunctionWorker(this, this.pathPrefix, name)
this.manifest!.functions[name].code!
);
this.runningFunctions.set(name, worker);
}
return await this.runningFunctions.get(name)!.invoke(args);
return await worker.invoke(args);
}
async dispatchEvent(name: string, data?: any): Promise<any[]> {
@ -137,13 +134,9 @@ export class Plug<HookT> {
export class System<HookT> {
protected plugs: Map<string, Plug<HookT>>;
protected pathPrefix: string;
registeredSyscalls: SysCallMapping;
plugLoader: PlugLoader<HookT>;
constructor(plugLoader: PlugLoader<HookT>, pathPrefix: string) {
this.plugLoader = plugLoader;
this.pathPrefix = pathPrefix;
constructor() {
this.plugs = new Map<string, Plug<HookT>>();
this.registeredSyscalls = {};
}
@ -168,7 +161,7 @@ export class System<HookT> {
}
async load(name: string, manifest: Manifest<HookT>): Promise<Plug<HookT>> {
const plug = new Plug(this, this.pathPrefix, name);
const plug = new Plug(this, name);
await plug.load(manifest);
this.plugs.set(name, plug);
return plug;

View File

@ -10,7 +10,6 @@ export interface Manifest<HookT> {
}
export interface FunctionDef {
path: string;
functionName?: string;
path?: string;
code?: string;
}

View File

@ -1,13 +1,6 @@
export function safeRun(fn: () => Promise<void>) {
fn().catch((e) => {
console.error(e);
});
}
export function sleep(ms: number): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, ms);
// console.error(e);
throw e;
});
}

View File

@ -1,16 +1,21 @@
export function syscall(name: string, ...args: any[]): any {
let reqId = Math.floor(Math.random() * 1000000);
// console.log("Syscall", name, reqId);
return new Promise((resolve, reject) => {
self.dispatchEvent(
new CustomEvent("syscall", {
detail: {
id: reqId,
name: name,
args: args,
callback: resolve,
},
})
);
});
declare global {
function syscall(id: string, name: string, args: any[]): Promise<any>;
}
export async function syscall(name: string, ...args: any[]): Promise<any> {
let reqId = "" + Math.floor(Math.random() * 1000000);
// console.log("Syscall", name, reqId);
return await self.syscall(reqId, name, args);
// return new Promise((resolve, reject) => {
// self.dispatchEvent(
// new CustomEvent("syscall", {
// detail: {
// id: reqId,
// name: name,
// args: args,
// callback: resolve,
// },
// })
// );
// });
}

View File

@ -19,6 +19,7 @@ async function navigate(syntaxNode: any) {
if (match) {
await syscall("editor.openUrl", match[1]);
}
break;
}
}

View File

@ -15,6 +15,7 @@ export async function indexLinks({ name, text }: IndexEvent) {
});
}
console.log("Found", backLinks.length, "wiki link(s)");
// throw Error("Boom");
await syscall("indexer.batchSet", name, backLinks);
}

View File

@ -7,8 +7,8 @@
"license": "MIT",
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
"start": "mkdir -p dist && cp src/function_worker.js dist/ && parcel",
"build": "parcel build && cp src/function_worker.js dist/",
"start": "parcel",
"build": "parcel build",
"clean": "rm -rf dist",
"check-watch": "tsc --noEmit --watch"
},

View File

@ -37,7 +37,7 @@ import { lineWrapper } from "./lineWrapper";
import { markdown } from "./markdown";
import { IPageNavigator, PathPageNavigator } from "./navigator";
import customMarkDown from "./parser";
import { BrowserSystem } from "./plugbox_browser/browser_system";
import { System } from "../../plugbox/src/runtime";
import { Plug } from "../../plugbox/src/runtime";
import { slashCommandRegexp } from "./types";
@ -124,7 +124,7 @@ export class Editor implements AppEventDispatcher {
}
async loadPlugs() {
const system = new BrowserSystem<NuggetHook>("/plug");
const system = new System<NuggetHook>();
system.registerSyscalls(
dbSyscalls,
editorSyscalls(this),
@ -132,7 +132,6 @@ export class Editor implements AppEventDispatcher {
indexerSyscalls(this.indexer)
);
await system.bootServiceWorker();
console.log("Now loading core plug");
let mainPlug = await system.load("core", coreManifest);
this.plugs.push(mainPlug);

View File

@ -1,76 +0,0 @@
// Page: this file is not built by Parcel, it's simply copied to the distribution
// The reason is that somehow Parcel cannot accept using importScripts otherwise
function safeRun(fn) {
fn().catch((e) => {
console.error(e);
});
}
let func = null;
let pendingRequests = {};
self.addEventListener("syscall", (event) => {
let customEvent = event;
let detail = customEvent.detail;
pendingRequests[detail.id] = detail.callback;
self.postMessage({
type: "syscall",
id: detail.id,
name: detail.name,
args: detail.args,
});
});
self.addEventListener("result", (event) => {
let customEvent = event;
self.postMessage({
type: "result",
result: customEvent.detail,
});
});
self.addEventListener("app-error", (event) => {
let customEvent = event;
self.postMessage({
type: "error",
reason: customEvent.detail,
});
});
self.addEventListener("message", (event) => {
safeRun(async () => {
let messageEvent = event;
let data = messageEvent.data;
switch (data.type) {
case "boot":
console.log("Booting", `${data.prefix}/function/${data.name}`);
importScripts(`${data.prefix}/function/${data.name}`);
self.postMessage({
type: "inited",
});
break;
case "invoke":
self.dispatchEvent(
new CustomEvent("invoke-function", {
detail: {
args: data.args || [],
},
})
);
break;
case "syscall-response":
let id = data.id;
const lookup = pendingRequests[id];
if (!lookup) {
console.log(
"Current outstanding requests",
pendingRequests,
"looking up",
id
);
throw Error("Invalid request id");
}
return await lookup(data.data);
}
});
});

View File

@ -1,57 +0,0 @@
import { PlugLoader, System } from "../../../plugbox/src/runtime";
import { Manifest } from "../../../plugbox/src/types";
import { sleep } from "../util";
export class BrowserLoader<HookT> implements PlugLoader<HookT> {
readonly pathPrefix: string;
constructor(pathPrefix: string) {
this.pathPrefix = pathPrefix;
}
async load(name: string, manifest: Manifest<HookT>): Promise<void> {
await fetch(`${this.pathPrefix}/${name}`, {
method: "PUT",
body: JSON.stringify(manifest),
});
}
}
export class BrowserSystem<HookT> extends System<HookT> {
constructor(pathPrefix: string) {
super(new BrowserLoader(pathPrefix), pathPrefix);
}
// Service worker stuff
async pollServiceWorkerActive() {
for (let i = 0; i < 25; i++) {
try {
console.log("Pinging...", `${this.pathPrefix}/$ping`);
let ping = await fetch(`${this.pathPrefix}/$ping`);
let text = await ping.text();
if (ping.status === 200 && text === "ok") {
return;
}
} catch (e) {
console.log("Not yet");
}
await sleep(100);
}
// Alright, something's messed up
throw new Error("Worker not successfully activated");
}
async bootServiceWorker() {
// @ts-ignore
let reg = navigator.serviceWorker.register(
new URL("../plugbox_sw.ts", import.meta.url),
{
type: "module",
scope: "/",
}
);
console.log("Service worker registered successfully");
await this.pollServiceWorkerActive();
}
}

View File

@ -1,108 +0,0 @@
import { Manifest } from "./types";
import { openDB } from "idb";
const rootUrl = location.origin + "/plug";
// Storing manifests in IndexedDB, y'all
const db = openDB("manifests-store", undefined, {
upgrade(db) {
db.createObjectStore("manifests");
},
});
async function saveManifest(name: string, manifest: Manifest) {
await (await db).put("manifests", manifest, name);
}
async function getManifest(name: string): Promise<Manifest | undefined> {
return (await (await db).get("manifests", name)) as Manifest | undefined;
}
self.addEventListener("install", (event) => {
console.log("Installing");
// @ts-ignore
self.skipWaiting();
});
async function handlePut(req: Request, path: string) {
console.log("Got manifest load for", path);
let manifest = (await req.json()) as Manifest;
await saveManifest(path, manifest);
// loadedBundles.set(path, manifest);
return new Response("ok");
}
function wrapScript(functionName: string, code: string): string {
return `const mod = ${code}
self.addEventListener('invoke-function', async e => {
try {
let result = await mod['${functionName}'](...e.detail.args);
self.dispatchEvent(new CustomEvent('result', {detail: result}));
} catch(e) {
console.error(\`Error while running ${functionName}\`, e);
self.dispatchEvent(new CustomEvent('app-error', {detail: e.message}));
}
});
`;
}
self.addEventListener("fetch", (event: any) => {
const req = event.request;
if (req.url.startsWith(rootUrl)) {
let path = req.url.substring(rootUrl.length + 1);
event.respondWith(
(async () => {
// console.log("Service worker is serving", path);
if (path === `$ping`) {
// console.log("Got ping");
return new Response("ok");
}
if (req.method === "PUT") {
return await handlePut(req, path);
}
let [plugName, resourceType, functionName] = path.split("/");
let manifest = await getManifest(plugName);
if (!manifest) {
// console.log("Ain't got", plugName);
return new Response(`Plug not loaded: ${plugName}`, {
status: 404,
});
}
if (resourceType === "$manifest") {
return new Response(JSON.stringify(manifest));
}
if (resourceType === "function") {
let func = manifest.functions[functionName];
// console.log("Serving function", functionName, func);
if (!func) {
return new Response("Not found", {
status: 404,
});
}
return new Response(wrapScript(func.functionName!, func.code!), {
status: 200,
headers: {
"Content-type": "application/javascript",
},
});
}
})()
);
}
});
self.addEventListener("activate", (event) => {
// console.log("Now ready to pick up fetches");
// @ts-ignore
event.waitUntil(self.clients.claim());
});
// console.log("I'm a service worker, look at me!", location.href);

View File

@ -13,15 +13,15 @@ export interface Space {
export class HttpRemoteSpace implements Space {
url: string;
socket: Socket;
socket?: Socket;
constructor(url: string, socket: Socket) {
constructor(url: string, socket: Socket | null) {
this.url = url;
this.socket = socket;
// this.socket = socket;
socket.on("connect", () => {
console.log("connected via SocketIO", serverEvents.pageText);
});
// socket.on("connect", () => {
// console.log("connected via SocketIO", serverEvents.pageText);
// });
}
async listPages(): Promise<PageMeta[]> {
@ -36,10 +36,10 @@ export class HttpRemoteSpace implements Space {
}
async openPage(name: string) {
this.socket.on(serverEvents.pageText, (pageName, text) => {
this.socket!.on(serverEvents.pageText, (pageName, text) => {
console.log("Got this", pageName, text);
});
this.socket.emit(serverEvents.openPage, "start");
this.socket!.emit(serverEvents.openPage, "start");
}
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {

View File

@ -14,14 +14,6 @@ export function safeRun(fn: () => Promise<void>) {
});
}
export function sleep(ms: number): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
export function isMacLike() {
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
}