From bf32d6d0bd8bfe877114c9085be5fcd9fa39db3e Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Mon, 28 Mar 2022 15:25:05 +0200 Subject: [PATCH] Plugin stuff --- package.json | 3 +- plugos/bin/plugos-bundle.ts | 7 +- plugos/environment/node_worker.ts | 2 +- plugos/package.json | 3 +- plugos/plug_loader.ts | 21 +++-- plugos/sandbox.ts | 1 + plugos/syscall/store.knex_node.ts | 12 ++- plugos/yarn.lock | 5 ++ plugs/core/core.plug.yaml | 22 +++-- plugs/core/markdown.ts | 23 ++++++ plugs/core/navigate.ts | 7 +- plugs/core/page.ts | 33 +++++++- plugs/core/task.ts | 31 ------- plugs/git/git.plug.yaml | 2 - plugs/markdown/markdown.plug.yaml | 6 ++ plugs/markdown/markdown.ts | 16 ++++ plugs/markdown/package.json | 12 +++ plugs/tasks/task.ts | 103 +++++++++++++++++++++++ plugs/tasks/tasks.plug.yaml | 14 ++++ server/api_server.ts | 2 +- server/index_api.ts | 15 +--- server/page_api.ts | 26 +++++- server/syscalls/page_index.ts | 133 +++++++++++++++++++----------- server/syscalls/space.ts | 27 ++++++ webapp/app_event.ts | 8 +- webapp/components/panel.tsx | 12 +++ webapp/constant.ts | 2 +- webapp/editor.tsx | 42 ++++++---- webapp/navigator.ts | 62 +++++--------- webapp/reducer.ts | 12 +++ webapp/space.ts | 2 +- webapp/styles/main.scss | 43 +++++----- webapp/syscalls/editor.ts | 16 +++- webapp/syscalls/system.ts | 2 +- webapp/types.ts | 10 ++- yarn.lock | 5 ++ 36 files changed, 523 insertions(+), 219 deletions(-) create mode 100644 plugs/core/markdown.ts delete mode 100644 plugs/core/task.ts create mode 100644 plugs/markdown/markdown.plug.yaml create mode 100644 plugs/markdown/markdown.ts create mode 100644 plugs/markdown/package.json create mode 100644 plugs/tasks/task.ts create mode 100644 plugs/tasks/tasks.plug.yaml create mode 100644 server/syscalls/space.ts create mode 100644 webapp/components/panel.tsx diff --git a/package.json b/package.json index c0b21f49..183f5eae 100644 --- a/package.json +++ b/package.json @@ -61,11 +61,13 @@ "body-parser": "^1.19.2", "buffer": "^6.0.3", "cors": "^2.8.5", + "events": "^3.3.0", "express": "^4.17.3", "jest": "^27.5.1", "knex": "^1.0.4", "node-cron": "^3.0.0", "node-fetch": "2", + "node-watch": "^0.7.3", "nodemon": "^2.0.15", "react": "^17.0.2", "react-dom": "^17.0.2", @@ -74,7 +76,6 @@ "supertest": "^6.2.2", "vm2": "^3.9.9", "yaml": "^1.10.2", - "events": "^3.3.0", "yargs": "^17.3.1" }, "devDependencies": { diff --git a/plugos/bin/plugos-bundle.ts b/plugos/bin/plugos-bundle.ts index 775b8d08..fe80a7e4 100755 --- a/plugos/bin/plugos-bundle.ts +++ b/plugos/bin/plugos-bundle.ts @@ -28,20 +28,19 @@ async function compile(filePath: string, functionName: string, debug: boolean) { bundle: true, format: "iife", globalName: "mod", - platform: "neutral", + platform: "browser", sourcemap: false, //sourceMap ? "inline" : false, minify: !debug, outfile: outFile, }); let jsCode = (await readFile(outFile)).toString(); - jsCode = jsCode.replace(/^var mod ?= ?/, ""); await unlink(outFile); if (inFile !== filePath) { await unlink(inFile); } - // Strip final ';' - return jsCode.substring(0, jsCode.length - 2); + return `(() => { ${jsCode} + return mod;})()`; } async function bundle(manifestPath: string, sourceMaps: boolean) { diff --git a/plugos/environment/node_worker.ts b/plugos/environment/node_worker.ts index ac594ca6..fa02d688 100644 --- a/plugos/environment/node_worker.ts +++ b/plugos/environment/node_worker.ts @@ -72,7 +72,6 @@ parentPort.on("message", (data: any) => { result: result && JSON.parse(JSON.stringify(result)), }); } catch (e: any) { - // console.log("ERROR", e); parentPort.postMessage({ type: "result", id: data.id, @@ -94,6 +93,7 @@ parentPort.on("message", (data: any) => { } pendingRequests.delete(syscallId); if (data.error) { + console.log("Got rejection", data.error); lookup.reject(new Error(data.error)); } else { lookup.resolve(data.result); diff --git a/plugos/package.json b/plugos/package.json index f6abc85c..4c711ff9 100644 --- a/plugos/package.json +++ b/plugos/package.json @@ -52,14 +52,15 @@ "knex": "^1.0.4", "node-cron": "^3.0.0", "node-fetch": "2", + "node-watch": "^0.7.3", "supertest": "^6.2.2", "vm2": "^3.9.9", "yaml": "^1.10.2", "yargs": "^17.3.1" }, "devDependencies": { - "@parcel/packager-raw-url": "2.3.2", "@parcel/optimizer-data-url": "2.3.2", + "@parcel/packager-raw-url": "2.3.2", "@parcel/service-worker": "2.3.2", "@parcel/transformer-inline-string": "2.3.2", "@parcel/transformer-sass": "2.3.2", diff --git a/plugos/plug_loader.ts b/plugos/plug_loader.ts index ff88a8bd..585263f8 100644 --- a/plugos/plug_loader.ts +++ b/plugos/plug_loader.ts @@ -1,4 +1,5 @@ -import fs, { watch } from "fs/promises"; +import fs from "fs/promises"; +import watch from "node-watch"; import path from "path"; import { createSandbox } from "./environment/node_sandbox"; import { safeRun } from "../server/util"; @@ -19,14 +20,15 @@ export class DiskPlugLoader { } watcher() { - safeRun(async () => { - for await (const { filename, eventType } of watch(this.plugPath)) { - if (!filename.endsWith(".plug.json")) { - return; - } + watch(this.plugPath, (eventType, localPath) => { + if (!localPath.endsWith(".plug.json")) { + return; + } + safeRun(async () => { try { - let localPath = path.join(this.plugPath, filename); + // let localPath = path.join(this.plugPath, filename); const plugName = extractPlugName(localPath); + console.log("Change detected for", plugName); try { await fs.stat(localPath); } catch (e) { @@ -34,10 +36,11 @@ export class DiskPlugLoader { await this.system.unload(plugName); } const plugDef = await this.loadPlugFromFile(localPath); - } catch { + } catch (e) { + console.log("Ignoring something FYI", e); // ignore, error handled by loadPlug } - } + }); }); } diff --git a/plugos/sandbox.ts b/plugos/sandbox.ts index c4b917ff..1ec78dba 100644 --- a/plugos/sandbox.ts +++ b/plugos/sandbox.ts @@ -71,6 +71,7 @@ export class Sandbox { result: result, } as WorkerMessage); } catch (e: any) { + // console.error("Syscall fail", e); this.worker.postMessage({ type: "syscall-response", id: data.id, diff --git a/plugos/syscall/store.knex_node.ts b/plugos/syscall/store.knex_node.ts index d8d8961f..5e4c448b 100644 --- a/plugos/syscall/store.knex_node.ts +++ b/plugos/syscall/store.knex_node.ts @@ -28,8 +28,8 @@ export function storeWriteSyscalls( tableName: string ): SysCallMapping { const apiObj: SysCallMapping = { - delete: async (ctx, page: string, key: string) => { - await db(tableName).where({ page, key }).del(); + delete: async (ctx, key: string) => { + await db(tableName).where({ key }).del(); }, deletePrefix: async (ctx, prefix: string) => { return db(tableName).andWhereLike("key", `${prefix}%`).del(); @@ -48,9 +48,15 @@ export function storeWriteSyscalls( }); } }, + // TODO: Optimize batchSet: async (ctx, kvs: KV[]) => { for (let { key, value } of kvs) { - await apiObj["store.set"](ctx, key, value); + await apiObj.set(ctx, key, value); + } + }, + batchDelete: async (ctx, keys: string[]) => { + for (let key of keys) { + await apiObj.delete(ctx, key); } }, }; diff --git a/plugos/yarn.lock b/plugos/yarn.lock index 9aeb2204..10ac9354 100644 --- a/plugos/yarn.lock +++ b/plugos/yarn.lock @@ -4005,6 +4005,11 @@ node-releases@^2.0.2: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== +node-watch@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/node-watch/-/node-watch-0.7.3.tgz#6d4db88e39c8d09d3ea61d6568d80e5975abc7ab" + integrity sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" diff --git a/plugs/core/core.plug.yaml b/plugs/core/core.plug.yaml index a4578948..bcfec855 100644 --- a/plugs/core/core.plug.yaml +++ b/plugs/core/core.plug.yaml @@ -1,4 +1,10 @@ functions: + clearPageIndex: + path: "./page.ts:clearPageIndex" + env: server + events: + - page:saved + - page:deleted indexLinks: path: "./page.ts:indexLinks" events: @@ -7,6 +13,13 @@ functions: path: "./page.ts:deletePage" command: name: "Page: Delete" + reindexSpaceCommand: + path: "./page.ts:reindexCommand" + command: + name: "Space: Reindex" + reindexSpace: + path: "./page.ts:reindexSpace" + env: server showBackLinks: path: "./page.ts:showBackLinks" command: @@ -29,10 +42,6 @@ functions: path: "./navigate.ts:clickNavigate" events: - page:click - taskToggle: - path: "./task.ts:taskToggle" - events: - - page:click insertToday: path: "./dates.ts:insertToday" command: @@ -43,4 +52,7 @@ functions: events: - plug:load env: server - +# renderMD: +# path: "./markdown.ts:renderMD" +# command: +# name: Render Markdown diff --git a/plugs/core/markdown.ts b/plugs/core/markdown.ts new file mode 100644 index 00000000..39b4208a --- /dev/null +++ b/plugs/core/markdown.ts @@ -0,0 +1,23 @@ +import { syscall } from "../lib/syscall"; +import mdParser from "../../webapp/parser"; + +export async function renderMD() { + let text = await syscall("editor.getText"); + let tree = mdParser.parser.parse(text); + let slicesToRemove: [number, number][] = []; + + tree.iterate({ + enter(type, from, to): false | void { + switch (type.name) { + case "Comment": + slicesToRemove.push([from, to]); + return false; + } + }, + }); + console.log("output peices", JSON.stringify(tree)); + slicesToRemove.reverse().forEach(([from, to]) => { + text = text.slice(0, from) + text.slice(to); + }); + console.log("Clean md", text); +} diff --git a/plugs/core/navigate.ts b/plugs/core/navigate.ts index 530c7ccd..3e0b01a8 100644 --- a/plugs/core/navigate.ts +++ b/plugs/core/navigate.ts @@ -8,7 +8,12 @@ async function navigate(syntaxNode: any) { console.log("Attempting to navigate based on syntax node", syntaxNode); switch (syntaxNode.name) { case "WikiLinkPage": - await syscall("editor.navigate", syntaxNode.text); + let pageLink = syntaxNode.text; + let pos = 0; + if (pageLink.includes("@")) { + [pageLink, pos] = syntaxNode.text.split("@"); + } + await syscall("editor.navigate", pageLink, +pos); break; case "URL": await syscall("editor.openUrl", syntaxNode.text); diff --git a/plugs/core/page.ts b/plugs/core/page.ts index 3c6866cc..3a54f33e 100644 --- a/plugs/core/page.ts +++ b/plugs/core/page.ts @@ -7,9 +7,12 @@ const wikilinkRegex = new RegExp(pageLinkRegex, "g"); export async function indexLinks({ name, text }: IndexEvent) { let backLinks: { key: string; value: string }[] = []; // [[Style Links]] - + console.log("Now indexing", name); for (let match of text.matchAll(wikilinkRegex)) { let toPage = match[1]; + if (toPage.includes("@")) { + toPage = toPage.split("@")[0]; + } let pos = match.index!; backLinks.push({ key: `pl:${toPage}:${pos}`, @@ -17,7 +20,6 @@ export async function indexLinks({ name, text }: IndexEvent) { }); } console.log("Found", backLinks.length, "wiki link(s)"); - // throw Error("Boom"); await syscall("indexer.batchSet", name, backLinks); } @@ -102,6 +104,29 @@ export async function showBackLinks() { console.log("Backlinks", backLinks); } -export async function reindex() { - await syscall("space.reindex"); +export async function reindexCommand() { + await syscall("editor.flashNotification", "Reindexing..."); + await syscall("system.invokeFunctionOnServer", "reindexSpace"); + await syscall("editor.flashNotification", "Reindexing done"); +} + +// Server functions +export async function reindexSpace() { + console.log("Clearing page index..."); + await syscall("indexer.clearPageIndex"); + console.log("Listing all pages"); + let pages = await syscall("space.listPages"); + for (let { name } of pages) { + console.log("Indexing", name); + const pageObj = await syscall("space.readPage", name); + await syscall("event.dispatch", "page:index", { + name, + text: pageObj.text, + }); + } +} + +export async function clearPageIndex(page: string) { + console.log("Clearing page index for page", page); + await syscall("indexer.clearPageIndexForPage", page); } diff --git a/plugs/core/task.ts b/plugs/core/task.ts deleted file mode 100644 index 8e375915..00000000 --- a/plugs/core/task.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { ClickEvent } from "../../webapp/app_event"; -import { syscall } from "../lib/syscall"; - -export async function taskToggle(event: ClickEvent) { - let syntaxNode = await syscall("editor.getSyntaxNodeAtPos", event.pos); - if (syntaxNode && syntaxNode.name === "TaskMarker") { - if (syntaxNode.text === "[x]" || syntaxNode.text === "[X]") { - await syscall("editor.dispatch", { - changes: { - from: syntaxNode.from, - to: syntaxNode.to, - insert: "[ ]", - }, - selection: { - anchor: event.pos, - }, - }); - } else { - await syscall("editor.dispatch", { - changes: { - from: syntaxNode.from, - to: syntaxNode.to, - insert: "[x]", - }, - selection: { - anchor: event.pos, - }, - }); - } - } -} diff --git a/plugs/git/git.plug.yaml b/plugs/git/git.plug.yaml index f199bf1c..08d6b129 100644 --- a/plugs/git/git.plug.yaml +++ b/plugs/git/git.plug.yaml @@ -14,8 +14,6 @@ functions: commit: path: "./git.ts:commit" env: server - cron: - - "*/15 * * * *" sync: path: "./git.ts:sync" env: server diff --git a/plugs/markdown/markdown.plug.yaml b/plugs/markdown/markdown.plug.yaml new file mode 100644 index 00000000..d3496a37 --- /dev/null +++ b/plugs/markdown/markdown.plug.yaml @@ -0,0 +1,6 @@ +functions: + mdTest: + path: "./markdown.ts:renderMarkdown" + env: client + command: + name: "Markdown: Render" diff --git a/plugs/markdown/markdown.ts b/plugs/markdown/markdown.ts new file mode 100644 index 00000000..1f99d686 --- /dev/null +++ b/plugs/markdown/markdown.ts @@ -0,0 +1,16 @@ +import MarkdownIt from "markdown-it"; +import { syscall } from "../lib/syscall"; + +var taskLists = require("markdown-it-task-lists"); + +const md = new MarkdownIt({ + linkify: true, + html: false, + typographer: true, +}).use(taskLists); + +export async function renderMarkdown() { + let text = await syscall("editor.getText"); + let html = md.render(text); + await syscall("editor.showRhs", `${html}`); +} diff --git a/plugs/markdown/package.json b/plugs/markdown/package.json new file mode 100644 index 00000000..568a843b --- /dev/null +++ b/plugs/markdown/package.json @@ -0,0 +1,12 @@ +{ + "name": "markdown", + "dependencies": { + "commonmark": "^0.30.0", + "markdown-it": "^12.3.2", + "markdown-it-task-lists": "^2.1.1" + }, + "devDependencies": { + "@types/commonmark": "^0.27.5", + "@types/markdown-it": "^12.2.3" + } +} diff --git a/plugs/tasks/task.ts b/plugs/tasks/task.ts new file mode 100644 index 00000000..09139cdf --- /dev/null +++ b/plugs/tasks/task.ts @@ -0,0 +1,103 @@ +import type { ClickEvent } from "../../webapp/app_event"; +import { IndexEvent } from "../../webapp/app_event"; +import { syscall } from "../lib/syscall"; + +const allTasksPageName = "ALL TASKS"; +const taskRe = /[\-\*]\s*\[([ Xx])\]\s*(.*)/g; +const extractPageLink = /[\-\*]\s*\[[ Xx]\]\s\[\[([^\]]+)@(\d+)\]\]\s*(.*)/; + +type Task = { task: string; complete: boolean; pos?: number }; + +export async function indexTasks({ name, text }: IndexEvent) { + if (name === allTasksPageName) { + return; + } + + console.log("Indexing tasks"); + let tasks: { key: string; value: Task }[] = []; + for (let match of text.matchAll(taskRe)) { + let complete = match[1] !== " "; + let task = match[2]; + let pos = match.index!; + tasks.push({ + key: `task:${pos}`, + value: { + task, + complete, + }, + }); + } + console.log("Found", tasks.length, "task(s)"); + await syscall("indexer.batchSet", name, tasks); +} + +export async function updateTaskPage() { + let allTasks = await syscall("indexer.scanPrefixGlobal", "task:"); + let pageTasks = new Map(); + for (let { + key, + page, + value: { task, complete, pos }, + } of allTasks) { + if (complete) { + continue; + } + let [, pos] = key.split(":"); + let tasks = pageTasks.get(page) || []; + tasks.push({ task, complete, pos }); + pageTasks.set(page, tasks); + } + + let mdPieces = []; + for (let pageName of [...pageTasks.keys()].sort()) { + mdPieces.push(`\n## ${pageName}\n`); + for (let task of pageTasks.get(pageName)!) { + mdPieces.push( + `* [${task.complete ? "x" : " "}] [[${pageName}@${task.pos}]] ${ + task.task + }` + ); + } + } + + let taskMd = mdPieces.join("\n"); + await syscall("space.writePage", allTasksPageName, taskMd); +} + +export async function taskToggle(event: ClickEvent) { + let syntaxNode = await syscall("editor.getSyntaxNodeAtPos", event.pos); + if (syntaxNode && syntaxNode.name === "TaskMarker") { + let changeTo = "[x]"; + if (syntaxNode.text === "[x]" || syntaxNode.text === "[X]") { + changeTo = "[ ]"; + } + await syscall("editor.dispatch", { + changes: { + from: syntaxNode.from, + to: syntaxNode.to, + insert: changeTo, + }, + selection: { + anchor: event.pos, + }, + }); + if (event.page === allTasksPageName) { + // Propagate back to the page in question + let line = (await syscall("editor.getLineUnderCursor")) as string; + let match = line.match(extractPageLink); + if (match) { + let [, page, posS] = match; + let pos = +posS; + let pageData = await syscall("space.readPage", page); + let text = pageData.text; + + // Apply the toggle + text = + text.substring(0, pos) + + text.substring(pos).replace(/^([\-\*]\s*)\[[ xX]\]/, "$1" + changeTo); + + await syscall("space.writePage", page, text); + } + } + } +} diff --git a/plugs/tasks/tasks.plug.yaml b/plugs/tasks/tasks.plug.yaml new file mode 100644 index 00000000..f0cff928 --- /dev/null +++ b/plugs/tasks/tasks.plug.yaml @@ -0,0 +1,14 @@ +functions: + indexTasks: + path: "./task.ts:indexTasks" + events: + - page:index + updateTaskPage: + path: "./task.ts:updateTaskPage" + command: + name: "Tasks: Update Page" + taskToggle: + path: "./task.ts:taskToggle" + events: + - page:click + diff --git a/server/api_server.ts b/server/api_server.ts index 522a873f..0c99be93 100644 --- a/server/api_server.ts +++ b/server/api_server.ts @@ -4,7 +4,7 @@ import * as path from "path"; import { IndexApi } from "./index_api"; import { PageApi } from "./page_api"; import { SilverBulletHooks } from "../common/manifest"; -import pageIndexSyscalls from "./syscalls/page_index"; +import { pageIndexSyscalls } from "./syscalls/page_index"; import { safeRun } from "./util"; import { System } from "../plugos/system"; diff --git a/server/index_api.ts b/server/index_api.ts index 8dd2c399..74b1664d 100644 --- a/server/index_api.ts +++ b/server/index_api.ts @@ -1,7 +1,7 @@ import { ApiProvider, ClientConnection } from "./api_server"; import knex, { Knex } from "knex"; import path from "path"; -import pageIndexSyscalls from "./syscalls/page_index"; +import { ensurePageIndexTable, pageIndexSyscalls } from "./syscalls/page_index"; type IndexItem = { page: string; @@ -10,7 +10,7 @@ type IndexItem = { }; export class IndexApi implements ApiProvider { - db: Knex; + db: Knex; constructor(rootPath: string) { this.db = knex({ @@ -23,15 +23,7 @@ export class IndexApi implements ApiProvider { } async init() { - if (!(await this.db.schema.hasTable("page_index"))) { - await this.db.schema.createTable("page_index", (table) => { - table.string("page"); - table.string("key"); - table.text("value"); - table.primary(["page", "key"]); - }); - console.log("Created table page_index"); - } + await ensurePageIndexTable(this.db); } api() { @@ -42,6 +34,7 @@ export class IndexApi implements ApiProvider { clientConn: ClientConnection, page: string ) => { + console.log("Now going to clear index for", page); return syscalls.clearPageIndexForPage(nullContext, page); }, set: async ( diff --git a/server/page_api.ts b/server/page_api.ts index 05e017a7..b0c630da 100644 --- a/server/page_api.ts +++ b/server/page_api.ts @@ -12,6 +12,8 @@ import { Cursor, cursorEffect } from "../webapp/cursorEffect"; import { SilverBulletHooks } from "../common/manifest"; import { System } from "../plugos/system"; import { EventFeature } from "../plugos/feature/event"; +import spaceSyscalls from "./syscalls/space"; +import { eventSyscalls } from "../plugos/syscall/event"; export class PageApi implements ApiProvider { openPages: Map; @@ -34,6 +36,8 @@ export class PageApi implements ApiProvider { this.system = system; this.eventFeature = new EventFeature(); system.addFeature(this.eventFeature); + system.registerSyscalls("space", [], spaceSyscalls(this)); + system.registerSyscalls("event", [], eventSyscalls(this.eventFeature)); } async init(): Promise { @@ -225,7 +229,10 @@ export class PageApi implements ApiProvider { " to disk and indexing." ); await this.flushPageToDisk(pageName, page); - + await this.eventFeature.dispatchEvent( + "page:saved", + pageName + ); await this.eventFeature.dispatchEvent("page:index", { name: pageName, text: page.text.sliceString(0), @@ -293,21 +300,32 @@ export class PageApi implements ApiProvider { pageName: string, text: string ) => { + // Write to disk + let pageMeta = await this.pageStore.writePage(pageName, text); + + // Notify clients that have the page open let page = this.openPages.get(pageName); if (page) { for (let client of page.clientStates) { - client.socket.emit("reloadPage", pageName); + client.socket.emit("pageChanged", pageMeta); } this.openPages.delete(pageName); } - return this.pageStore.writePage(pageName, text); + // Trigger system events + await this.eventFeature.dispatchEvent("page:saved", pageName); + await this.eventFeature.dispatchEvent("page:index", { + name: pageName, + text: text, + }); + return pageMeta; }, deletePage: async (clientConn: ClientConnection, pageName: string) => { this.openPages.delete(pageName); clientConn.openPages.delete(pageName); // Cascading of this to all connected clients will be handled by file watcher - return this.pageStore.deletePage(pageName); + await this.pageStore.deletePage(pageName); + await this.eventFeature.dispatchEvent("page:deleted", pageName); }, listPages: async (clientConn: ClientConnection): Promise => { diff --git a/server/syscalls/page_index.ts b/server/syscalls/page_index.ts index b532239e..2dc076b4 100644 --- a/server/syscalls/page_index.ts +++ b/server/syscalls/page_index.ts @@ -1,6 +1,12 @@ import { Knex } from "knex"; import { SysCallMapping } from "../../plugos/system"; +import { + ensureTable, + storeReadSyscalls, + storeWriteSyscalls, +} from "../../plugos/syscall/store.knex_node"; + type IndexItem = { page: string; key: string; @@ -12,72 +18,99 @@ export type KV = { value: any; }; -export default function (db: Knex): SysCallMapping { +/* + Keyspace design: + + for page lookups: + p~page~key + + for global lookups: + k~key~page + + */ + +function pageKey(page: string, key: string) { + return `p~${page}~${key}`; +} + +function unpackPageKey(dbKey: string): { page: string; key: string } { + const [, page, key] = dbKey.split("~"); + return { page, key }; +} + +function globalKey(page: string, key: string) { + return `k~${key}~${page}`; +} + +function unpackGlobalKey(dbKey: string): { page: string; key: string } { + const [, key, page] = dbKey.split("~"); + return { page, key }; +} + +export async function ensurePageIndexTable(db: Knex) { + await ensureTable(db, "page_index"); +} + +export function pageIndexSyscalls(db: Knex): SysCallMapping { + const readCalls = storeReadSyscalls(db, "page_index"); + const writeCalls = storeWriteSyscalls(db, "page_index"); const apiObj: SysCallMapping = { - clearPageIndexForPage: async (ctx, page: string) => { - await db("page_index").where({ page }).del(); - }, set: async (ctx, page: string, key: string, value: any) => { - let changed = await db("page_index") - .where({ page, key }) - .update("value", JSON.stringify(value)); - if (changed === 0) { - await db("page_index").insert({ - page, - key, - value: JSON.stringify(value), - }); - } + await writeCalls.set(ctx, pageKey(page, key), value); + await writeCalls.set(ctx, globalKey(page, key), value); }, batchSet: async (ctx, page: string, kvs: KV[]) => { for (let { key, value } of kvs) { await apiObj.set(ctx, page, key, value); } }, - get: async (ctx, page: string, key: string) => { - let result = await db("page_index") - .where({ page, key }) - .select("value"); - if (result.length) { - return JSON.parse(result[0].value); - } else { - return null; - } - }, delete: async (ctx, page: string, key: string) => { - await db("page_index").where({ page, key }).del(); + await writeCalls.delete(ctx, pageKey(page, key)); + await writeCalls.delete(ctx, globalKey(page, key)); + }, + get: async (ctx, page: string, key: string) => { + return readCalls.get(ctx, pageKey(page, key)); }, scanPrefixForPage: async (ctx, page: string, prefix: string) => { - return ( - await db("page_index") - .where({ page }) - .andWhereLike("key", `${prefix}%`) - .select("page", "key", "value") - ).map(({ page, key, value }) => ({ - page, - key, - value: JSON.parse(value), - })); + return (await readCalls.queryPrefix(ctx, pageKey(page, prefix))).map( + ({ key, value }: { key: string; value: any }) => { + const { key: pageKey } = unpackPageKey(key); + return { + page, + key: pageKey, + value, + }; + } + ); }, scanPrefixGlobal: async (ctx, prefix: string) => { - return ( - await db("page_index") - .andWhereLike("key", `${prefix}%`) - .select("page", "key", "value") - ).map(({ page, key, value }) => ({ - page, - key, - value: JSON.parse(value), - })); + return (await readCalls.queryPrefix(ctx, `k~${prefix}`)).map( + ({ key, value }: { key: string; value: any }) => { + const { page, key: pageKey } = unpackGlobalKey(key); + return { + page, + key: pageKey, + value, + }; + } + ); + }, + clearPageIndexForPage: async (ctx, page: string) => { + await apiObj.deletePrefixForPage(ctx, page, ""); }, deletePrefixForPage: async (ctx, page: string, prefix: string) => { - return db("page_index") - .where({ page }) - .andWhereLike("key", `${prefix}%`) - .del(); + // Collect all global keys for this page to delete + let keysToDelete = ( + await readCalls.queryPrefix(ctx, pageKey(page, prefix)) + ).map(({ key }: { key: string; value: string }) => + globalKey(page, unpackPageKey(key).key) + ); + // Delete all page keys + await writeCalls.deletePrefix(ctx, pageKey(page, prefix)); + await writeCalls.batchDelete(ctx, keysToDelete); }, - clearPageIndex: async () => { - return db("page_index").del(); + clearPageIndex: async (ctx) => { + await writeCalls.deleteAll(ctx); }, }; return apiObj; diff --git a/server/syscalls/space.ts b/server/syscalls/space.ts new file mode 100644 index 00000000..7e73ccae --- /dev/null +++ b/server/syscalls/space.ts @@ -0,0 +1,27 @@ +import { PageMeta } from "../types"; +import { SysCallMapping } from "../../plugos/system"; +import { PageApi } from "../page_api"; +import { ClientConnection } from "../api_server"; + +export default (pageApi: PageApi): SysCallMapping => { + const api = pageApi.api(); + // @ts-ignore + const dummyConn = new ClientConnection(null); + return { + listPages: (ctx): Promise => { + return api.listPages(dummyConn); + }, + readPage: async ( + ctx, + name: string + ): Promise<{ text: string; meta: PageMeta }> => { + return api.readPage(dummyConn, name); + }, + writePage: async (ctx, name: string, text: string): Promise => { + return api.writePage(dummyConn, name, text); + }, + deletePage: async (ctx, name: string) => { + return api.deletePage(dummyConn, name); + }, + }; +}; diff --git a/webapp/app_event.ts b/webapp/app_event.ts index 2ca8c13f..984430a2 100644 --- a/webapp/app_event.ts +++ b/webapp/app_event.ts @@ -1,11 +1,7 @@ -export type AppEvent = - | "app:ready" - | "page:save" - | "page:click" - | "page:index" - | "editor:complete"; +export type AppEvent = "page:click" | "editor:complete"; export type ClickEvent = { + page: string; pos: number; metaKey: boolean; ctrlKey: boolean; diff --git a/webapp/components/panel.tsx b/webapp/components/panel.tsx new file mode 100644 index 00000000..e34813ae --- /dev/null +++ b/webapp/components/panel.tsx @@ -0,0 +1,12 @@ +import { useRef } from "react"; + +export function Panel({ html }: { html: string }) { + const iFrameRef = useRef(null); + // @ts-ignore + window.iframeRef = iFrameRef; + return ( +
+