Work on sync stuff
This commit is contained in:
parent
1b0048cdcf
commit
38faf50ab8
4
.idea/silverbullet.iml
generated
4
.idea/silverbullet.iml
generated
@ -2,7 +2,9 @@
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/dist" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
|
@ -35,7 +35,7 @@
|
||||
"context": "node"
|
||||
},
|
||||
"test": {
|
||||
"source": ["plugs/lib/tree.test.ts"],
|
||||
"source": ["plugs/lib/tree.test.ts", "webapp/spaces/sync.test.ts"],
|
||||
"outputFormat": "commonjs",
|
||||
"isLibrary": true,
|
||||
"context": "node"
|
||||
@ -64,6 +64,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"events": "^3.3.0",
|
||||
"express": "^4.17.3",
|
||||
"dexie": "^3.2.1",
|
||||
"jest": "^27.5.1",
|
||||
"knex": "^1.0.4",
|
||||
"node-cron": "^3.0.0",
|
||||
@ -75,6 +76,8 @@
|
||||
"supertest": "^6.2.2",
|
||||
"vm2": "^3.9.9",
|
||||
"yaml": "^1.10.2",
|
||||
"fake-indexeddb": "^3.1.7",
|
||||
|
||||
"yargs": "^17.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {syscall} from "./syscall";
|
||||
import { syscall } from "./syscall";
|
||||
|
||||
export function getCurrentPage(): Promise<string> {
|
||||
return syscall("editor.getCurrentPage");
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {syscall} from "./syscall";
|
||||
import type {MarkdownTree} from "../common/tree";
|
||||
import { syscall } from "./syscall";
|
||||
import type { MarkdownTree } from "../common/tree";
|
||||
|
||||
export async function parseMarkdown(text: string): Promise<MarkdownTree> {
|
||||
return syscall("markdown.parseMarkdown", text);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { syscall } from "./syscall";
|
||||
|
||||
export async function invokeFunctionOnServer(
|
||||
export async function invokeFunction(
|
||||
env: string,
|
||||
name: string,
|
||||
...args: any[]
|
||||
): Promise<any> {
|
||||
|
@ -2,18 +2,18 @@
|
||||
|
||||
import express from "express";
|
||||
import yargs from "yargs";
|
||||
import {hideBin} from "yargs/helpers";
|
||||
import {DiskPlugLoader} from "../plug_loader";
|
||||
import {CronHookT, NodeCronHook} from "../hooks/node_cron";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import { DiskPlugLoader } from "../plug_loader";
|
||||
import { CronHookT, NodeCronHook } from "../hooks/node_cron";
|
||||
import shellSyscalls from "../syscalls/shell.node";
|
||||
import {System} from "../system";
|
||||
import {EndpointHook, EndpointHookT} from "../hooks/endpoint";
|
||||
import {safeRun} from "../util";
|
||||
import { System } from "../system";
|
||||
import { EndpointHook, EndpointHookT } from "../hooks/endpoint";
|
||||
import { safeRun } from "../util";
|
||||
import knex from "knex";
|
||||
import {ensureTable, storeSyscalls} from "../syscalls/store.knex_node";
|
||||
import {fetchSyscalls} from "../syscalls/fetch.node";
|
||||
import {EventHook, EventHookT} from "../hooks/event";
|
||||
import {eventSyscalls} from "../syscalls/event";
|
||||
import { ensureTable, storeSyscalls } from "../syscalls/store.knex_node";
|
||||
import { fetchSyscalls } from "../syscalls/fetch.node";
|
||||
import { EventHook, EventHookT } from "../hooks/event";
|
||||
import { eventSyscalls } from "../syscalls/event";
|
||||
|
||||
let args = yargs(hideBin(process.argv))
|
||||
.option("port", {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {safeRun} from "../util";
|
||||
import {ControllerMessage, WorkerMessage} from "./worker";
|
||||
import { safeRun } from "../util";
|
||||
import { ControllerMessage, WorkerMessage } from "./worker";
|
||||
|
||||
let loadedFunctions = new Map<string, Function>();
|
||||
let pendingRequests = new Map<
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Hook, Manifest} from "../types";
|
||||
import {System} from "../system";
|
||||
import {safeRun} from "../util";
|
||||
import { Hook, Manifest } from "../types";
|
||||
import { System } from "../system";
|
||||
import { safeRun } from "../util";
|
||||
|
||||
// System events:
|
||||
// - plug:load (plugName: string)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {createSandbox} from "./environments/node_sandbox";
|
||||
import {expect, test} from "@jest/globals";
|
||||
import {System} from "./system";
|
||||
import { createSandbox } from "./environments/node_sandbox";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { System } from "./system";
|
||||
|
||||
test("Run a Node sandbox", async () => {
|
||||
let system = new System("server");
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {SysCallMapping} from "../system";
|
||||
import {EventHook} from "../hooks/event";
|
||||
import { SysCallMapping } from "../system";
|
||||
import { EventHook } from "../hooks/event";
|
||||
|
||||
export function eventSyscalls(eventHook: EventHook): SysCallMapping {
|
||||
return {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import fetch, {RequestInfo, RequestInit} from "node-fetch";
|
||||
import {SysCallMapping} from "../system";
|
||||
import fetch, { RequestInfo, RequestInit } from "node-fetch";
|
||||
import { SysCallMapping } from "../system";
|
||||
|
||||
export function fetchSyscalls(): SysCallMapping {
|
||||
return {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {promisify} from "util";
|
||||
import {execFile} from "child_process";
|
||||
import type {SysCallMapping} from "../system";
|
||||
import { promisify } from "util";
|
||||
import { execFile } from "child_process";
|
||||
import type { SysCallMapping } from "../system";
|
||||
|
||||
const execFilePromise = promisify(execFile);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {createSandbox} from "../environments/node_sandbox";
|
||||
import {expect, test} from "@jest/globals";
|
||||
import {System} from "../system";
|
||||
import {storeSyscalls} from "./store.dexie_browser";
|
||||
import { createSandbox } from "../environments/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");
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Dexie from "dexie";
|
||||
import {SysCallMapping} from "../system";
|
||||
import { SysCallMapping } from "../system";
|
||||
|
||||
export type KV = {
|
||||
key: string;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {createSandbox} from "../environments/node_sandbox";
|
||||
import {expect, test} from "@jest/globals";
|
||||
import {System} from "../system";
|
||||
import {ensureTable, storeSyscalls} from "./store.knex_node";
|
||||
import { createSandbox } from "../environments/node_sandbox";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { System } from "../system";
|
||||
import { ensureTable, storeSyscalls } from "./store.knex_node";
|
||||
import knex from "knex";
|
||||
import fs from "fs/promises";
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Knex} from "knex";
|
||||
import {SysCallMapping} from "../system";
|
||||
import { Knex } from "knex";
|
||||
import { SysCallMapping } from "../system";
|
||||
|
||||
type Item = {
|
||||
page: string;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SyscallContext, SysCallMapping } from "../system";
|
||||
|
||||
export function transportSyscalls(
|
||||
export function proxySyscalls(
|
||||
names: string[],
|
||||
transportCall: (
|
||||
ctx: SyscallContext,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Hook, Manifest, RuntimeEnvironment} from "./types";
|
||||
import {EventEmitter} from "../common/event";
|
||||
import {SandboxFactory} from "./sandbox";
|
||||
import {Plug} from "./plug";
|
||||
import { Hook, Manifest, RuntimeEnvironment } from "./types";
|
||||
import { EventEmitter } from "../common/event";
|
||||
import { SandboxFactory } from "./sandbox";
|
||||
import { Plug } from "./plug";
|
||||
|
||||
export interface SysCallMapping {
|
||||
[key: string]: (ctx: SyscallContext, ...args: any) => Promise<any> | any;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {IndexEvent} from "../../webapp/app_event";
|
||||
import {whiteOutQueries} from "./materialized_queries";
|
||||
import { IndexEvent } from "../../webapp/app_event";
|
||||
import { whiteOutQueries } from "./materialized_queries";
|
||||
|
||||
import {batchSet} from "plugos-silverbullet-syscall/index";
|
||||
import {parseMarkdown} from "plugos-silverbullet-syscall/markdown";
|
||||
import {collectNodesMatching, MarkdownTree, renderMarkdown,} from "../lib/tree";
|
||||
import { batchSet } from "plugos-silverbullet-syscall/index";
|
||||
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
|
||||
import { collectNodesMatching, MarkdownTree, renderMarkdown } from "../lib/tree";
|
||||
|
||||
type Item = {
|
||||
item: string;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {flashNotification, getCurrentPage, reloadPage, save,} from "plugos-silverbullet-syscall/editor";
|
||||
import { flashNotification, getCurrentPage, reloadPage, save } from "plugos-silverbullet-syscall/editor";
|
||||
|
||||
import {listPages, readPage, writePage,} from "plugos-silverbullet-syscall/space";
|
||||
import {invokeFunctionOnServer} from "plugos-silverbullet-syscall/system";
|
||||
import {scanPrefixGlobal} from "plugos-silverbullet-syscall";
|
||||
import { listPages, readPage, writePage } from "plugos-silverbullet-syscall/space";
|
||||
import { invokeFunction } from "plugos-silverbullet-syscall/system";
|
||||
import { scanPrefixGlobal } from "plugos-silverbullet-syscall";
|
||||
|
||||
export const queryRegex =
|
||||
/(<!--\s*#query\s+(?<table>\w+)\s*(filter\s+["'“”‘’](?<filter>[^"'“”‘’]+)["'“”‘’])?\s*\s*(order by\s+(?<orderBy>\w+)(?<orderDesc>\s+desc)?)?(group by\s+(?<groupBy>\w+))?\s*(limit\s+(?<limit>\d+))?\s*-->)(.+?)(<!--\s*#end\s*-->)/gs;
|
||||
@ -31,7 +31,11 @@ async function replaceAsync(
|
||||
export async function updateMaterializedQueriesCommand() {
|
||||
const currentPage = await getCurrentPage();
|
||||
await save();
|
||||
await invokeFunctionOnServer("updateMaterializedQueriesOnPage", currentPage);
|
||||
await invokeFunction(
|
||||
"server",
|
||||
"updateMaterializedQueriesOnPage",
|
||||
currentPage
|
||||
);
|
||||
await reloadPage();
|
||||
await flashNotification("Updated materialized queries");
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {ClickEvent} from "../../webapp/app_event";
|
||||
import {updateMaterializedQueriesCommand} from "./materialized_queries";
|
||||
import {getCursor, getText, navigate as navigateTo, openUrl,} from "plugos-silverbullet-syscall/editor";
|
||||
import {taskToggleAtPos} from "../tasks/task";
|
||||
import {parseMarkdown} from "plugos-silverbullet-syscall/markdown";
|
||||
import {MarkdownTree, nodeAtPos} from "../lib/tree";
|
||||
import { ClickEvent } from "../../webapp/app_event";
|
||||
import { updateMaterializedQueriesCommand } from "./materialized_queries";
|
||||
import { getCursor, getText, navigate as navigateTo, openUrl } from "plugos-silverbullet-syscall/editor";
|
||||
import { taskToggleAtPos } from "../tasks/task";
|
||||
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
|
||||
import { MarkdownTree, nodeAtPos } from "../lib/tree";
|
||||
|
||||
const materializedQueryPrefix = /<!--\s*#query\s+/;
|
||||
|
||||
|
@ -4,24 +4,13 @@ import {
|
||||
batchSet,
|
||||
clearPageIndex as clearPageIndexSyscall,
|
||||
clearPageIndexForPage,
|
||||
scanPrefixGlobal,
|
||||
scanPrefixGlobal
|
||||
} from "plugos-silverbullet-syscall/index";
|
||||
import {
|
||||
flashNotification,
|
||||
getCurrentPage,
|
||||
getText,
|
||||
matchBefore,
|
||||
navigate,
|
||||
} from "plugos-silverbullet-syscall/editor";
|
||||
import { flashNotification, getCurrentPage, getText, matchBefore, navigate } from "plugos-silverbullet-syscall/editor";
|
||||
|
||||
import { dispatch } from "plugos-syscall/event";
|
||||
import {
|
||||
deletePage as deletePageSyscall,
|
||||
listPages,
|
||||
readPage,
|
||||
writePage,
|
||||
} from "plugos-silverbullet-syscall/space";
|
||||
import { invokeFunctionOnServer } from "plugos-silverbullet-syscall/system";
|
||||
import { deletePage as deletePageSyscall, listPages, readPage, writePage } from "plugos-silverbullet-syscall/space";
|
||||
import { invokeFunction } from "plugos-silverbullet-syscall/system";
|
||||
|
||||
const wikilinkRegex = new RegExp(pageLinkRegex, "g");
|
||||
|
||||
@ -120,7 +109,7 @@ export async function showBackLinks() {
|
||||
|
||||
export async function reindexCommand() {
|
||||
await flashNotification("Reindexing...");
|
||||
await invokeFunctionOnServer("reindexSpace");
|
||||
await invokeFunction("server", "reindexSpace");
|
||||
await flashNotification("Reindexing done");
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// @ts-ignore
|
||||
import emojis from "./emoji.json";
|
||||
import {matchBefore} from "plugos-silverbullet-syscall/editor";
|
||||
import { matchBefore } from "plugos-silverbullet-syscall/editor";
|
||||
|
||||
const emojiMatcher = /\(([^\)]+)\)\s+(.+)$/;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { run } from "plugos-syscall/shell";
|
||||
import { flashNotification, prompt } from "plugos-silverbullet-syscall/editor";
|
||||
import { invokeFunctionOnServer } from "plugos-silverbullet-syscall/system";
|
||||
import { invokeFunction } from "plugos-silverbullet-syscall/system";
|
||||
|
||||
export async function commit(message?: string) {
|
||||
if (!message) {
|
||||
@ -25,12 +25,12 @@ export async function snapshotCommand() {
|
||||
revName = "Snapshot";
|
||||
}
|
||||
console.log("Revision name", revName);
|
||||
await invokeFunctionOnServer("commit", revName);
|
||||
await invokeFunction("server", "commit", revName);
|
||||
}
|
||||
|
||||
export async function syncCommand() {
|
||||
await flashNotification("Syncing with git");
|
||||
await invokeFunctionOnServer("sync");
|
||||
await invokeFunction("server", "sync");
|
||||
await flashNotification("Git sync complete!");
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {expect, test} from "@jest/globals";
|
||||
import {parse} from "../../common/tree";
|
||||
import {addParentPointers, collectNodesMatching, findParentMatching, nodeAtPos, renderMarkdown,} from "./tree";
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { parse } from "../../common/tree";
|
||||
import { addParentPointers, collectNodesMatching, findParentMatching, nodeAtPos, renderMarkdown } from "./tree";
|
||||
|
||||
const mdTest1 = `
|
||||
# Heading
|
||||
|
@ -1,8 +1,8 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
import {getText, hideRhs, showRhs} from "plugos-silverbullet-syscall/editor";
|
||||
import { getText, hideRhs, showRhs } from "plugos-silverbullet-syscall/editor";
|
||||
import * as clientStore from "plugos-silverbullet-syscall/clientStore";
|
||||
import {parseMarkdown} from "plugos-silverbullet-syscall/markdown";
|
||||
import {addParentPointers, renderMarkdown, replaceNodesMatching,} from "../lib/tree";
|
||||
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
|
||||
import { addParentPointers, renderMarkdown, replaceNodesMatching } from "../lib/tree";
|
||||
|
||||
var taskLists = require("markdown-it-task-lists");
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import type {ClickEvent} from "../../webapp/app_event";
|
||||
import {IndexEvent} from "../../webapp/app_event";
|
||||
import type { ClickEvent } from "../../webapp/app_event";
|
||||
import { IndexEvent } from "../../webapp/app_event";
|
||||
|
||||
import {whiteOutQueries} from "../core/materialized_queries";
|
||||
import {batchSet} from "plugos-silverbullet-syscall/index";
|
||||
import {readPage, writePage} from "plugos-silverbullet-syscall/space";
|
||||
import {parseMarkdown} from "plugos-silverbullet-syscall/markdown";
|
||||
import {dispatch, getText} from "plugos-silverbullet-syscall/editor";
|
||||
import {addParentPointers, collectNodesMatching, nodeAtPos, renderMarkdown,} from "../lib/tree";
|
||||
import { whiteOutQueries } from "../core/materialized_queries";
|
||||
import { batchSet } from "plugos-silverbullet-syscall/index";
|
||||
import { readPage, writePage } from "plugos-silverbullet-syscall/space";
|
||||
import { parseMarkdown } from "plugos-silverbullet-syscall/markdown";
|
||||
import { dispatch, getText } from "plugos-silverbullet-syscall/editor";
|
||||
import { addParentPointers, collectNodesMatching, nodeAtPos, renderMarkdown } from "../lib/tree";
|
||||
|
||||
type Task = {
|
||||
task: string;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Knex} from "knex";
|
||||
import {SysCallMapping} from "../../plugos/system";
|
||||
import { Knex } from "knex";
|
||||
import { SysCallMapping } from "../../plugos/system";
|
||||
|
||||
import {ensureTable, storeSyscalls,} from "../../plugos/syscalls/store.knex_node";
|
||||
import { ensureTable, storeSyscalls } from "../../plugos/syscalls/store.knex_node";
|
||||
|
||||
type IndexItem = {
|
||||
page: string;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {PageMeta} from "../../common/types";
|
||||
import {SysCallMapping} from "../../plugos/system";
|
||||
import {Storage} from "../disk_storage";
|
||||
import { PageMeta } from "../../common/types";
|
||||
import { SysCallMapping } from "../../plugos/system";
|
||||
import { Storage } from "../disk_storage";
|
||||
|
||||
export default (storage: Storage): SysCallMapping => {
|
||||
return {
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { Editor } from "./editor";
|
||||
import { Space } from "./space";
|
||||
import { safeRun } from "./util";
|
||||
import { IndexedDBSpace } from "./spaces/indexeddb_space";
|
||||
|
||||
let editor = new Editor(new Space(""), document.getElementById("root")!);
|
||||
let editor = new Editor(
|
||||
// new HttpRestSpace(""),
|
||||
new IndexedDBSpace("pages"),
|
||||
document.getElementById("root")!
|
||||
);
|
||||
|
||||
safeRun(async () => {
|
||||
await editor.init();
|
||||
|
@ -2,18 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link href="panel.scss" rel="stylesheet" />
|
||||
<link href="panel.scss" rel="stylesheet"/>
|
||||
<base target="_top">
|
||||
<script type="module">
|
||||
window.addEventListener("message", (message) => {
|
||||
const data = message.data;
|
||||
switch(data.type) {
|
||||
case "html":
|
||||
document.body.innerHTML = data.html;
|
||||
break;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<script src="panel_page.ts"/>
|
||||
</head>
|
||||
<body>
|
||||
Send me HTML
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {useEffect, useRef} from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
// @ts-ignore
|
||||
import iframeHtml from "bundle-text:./panel.html";
|
||||
|
||||
@ -23,6 +23,22 @@ export function Panel({ html, flex }: { html: string; flex: number }) {
|
||||
iframe.onload = null;
|
||||
};
|
||||
}, [html]);
|
||||
|
||||
useEffect(() => {
|
||||
let messageListener = (evt: any) => {
|
||||
if (evt.source !== iFrameRef.current!.contentWindow) {
|
||||
return;
|
||||
}
|
||||
let data = evt.data;
|
||||
if (!data) return;
|
||||
console.log("Got message from panel", data);
|
||||
};
|
||||
window.addEventListener("message", messageListener);
|
||||
return () => {
|
||||
window.removeEventListener("message", messageListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="panel" style={{ flex }}>
|
||||
<iframe srcDoc={iframeHtml} ref={iFrameRef} />
|
||||
|
22
webapp/components/panel_page.ts
Normal file
22
webapp/components/panel_page.ts
Normal file
@ -0,0 +1,22 @@
|
||||
window.addEventListener("message", (message) => {
|
||||
const data = message.data;
|
||||
switch (data.type) {
|
||||
case "html":
|
||||
document.body.innerHTML = data.html;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function sendEvent(data: any) {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: "event",
|
||||
data: data,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
//
|
||||
// setInterval(() => {
|
||||
// self.sendEvent("testing");
|
||||
// }, 2000);
|
@ -1,4 +1,4 @@
|
||||
import {EditorView} from "@codemirror/view";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import * as util from "../util";
|
||||
|
||||
export function StatusBar({ editorView }: { editorView?: EditorView }) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Notification} from "../types";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFileLines} from "@fortawesome/free-solid-svg-icons";
|
||||
import { Notification } from "../types";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFileLines } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
function prettyName(s: string | undefined): string {
|
||||
if (!s) {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {autocompletion, completionKeymap} from "@codemirror/autocomplete";
|
||||
import {closeBrackets, closeBracketsKeymap} from "@codemirror/closebrackets";
|
||||
import {indentWithTab, standardKeymap} from "@codemirror/commands";
|
||||
import {history, historyKeymap} from "@codemirror/history";
|
||||
import {bracketMatching} from "@codemirror/matchbrackets";
|
||||
import {searchKeymap} from "@codemirror/search";
|
||||
import {EditorSelection, EditorState} from "@codemirror/state";
|
||||
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
||||
import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets";
|
||||
import { indentWithTab, standardKeymap } from "@codemirror/commands";
|
||||
import { history, historyKeymap } from "@codemirror/history";
|
||||
import { bracketMatching } from "@codemirror/matchbrackets";
|
||||
import { searchKeymap } from "@codemirror/search";
|
||||
import { EditorSelection, EditorState } from "@codemirror/state";
|
||||
import {
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
@ -13,41 +13,41 @@ import {
|
||||
KeyBinding,
|
||||
keymap,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
ViewUpdate
|
||||
} from "@codemirror/view";
|
||||
import React, {useEffect, useReducer} from "react";
|
||||
import React, { useEffect, useReducer } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {createSandbox as createIFrameSandbox} from "../plugos/environments/webworker_sandbox";
|
||||
import {AppEvent, AppEventDispatcher, ClickEvent} from "./app_event";
|
||||
import { createSandbox as createIFrameSandbox } from "../plugos/environments/webworker_sandbox";
|
||||
import { AppEvent, AppEventDispatcher, ClickEvent } from "./app_event";
|
||||
import * as commands from "./commands";
|
||||
import {CommandPalette} from "./components/command_palette";
|
||||
import {PageNavigator} from "./components/page_navigator";
|
||||
import {TopBar} from "./components/top_bar";
|
||||
import {lineWrapper} from "./line_wrapper";
|
||||
import {markdown} from "./markdown";
|
||||
import {PathPageNavigator} from "./navigator";
|
||||
import { CommandPalette } from "./components/command_palette";
|
||||
import { PageNavigator } from "./components/page_navigator";
|
||||
import { TopBar } from "./components/top_bar";
|
||||
import { lineWrapper } from "./line_wrapper";
|
||||
import { markdown } from "./markdown";
|
||||
import { PathPageNavigator } from "./navigator";
|
||||
import customMarkDown from "./parser";
|
||||
import reducer from "./reducer";
|
||||
import {smartQuoteKeymap} from "./smart_quotes";
|
||||
import {Space} from "./space";
|
||||
import { smartQuoteKeymap } from "./smart_quotes";
|
||||
import { Space } from "./spaces/space";
|
||||
import customMarkdownStyle from "./style";
|
||||
import {editorSyscalls} from "./syscalls/editor";
|
||||
import {indexerSyscalls} from "./syscalls/indexer";
|
||||
import {spaceSyscalls} from "./syscalls/space";
|
||||
import {Action, AppViewState, initialViewState} from "./types";
|
||||
import {SilverBulletHooks} from "../common/manifest";
|
||||
import {safeRun, throttle} from "./util";
|
||||
import {System} from "../plugos/system";
|
||||
import {EventHook} from "../plugos/hooks/event";
|
||||
import {systemSyscalls} from "./syscalls/system";
|
||||
import {Panel} from "./components/panel";
|
||||
import {CommandHook} from "./hooks/command";
|
||||
import {SlashCommandHook} from "./hooks/slash_command";
|
||||
import {CompleterHook} from "./hooks/completer";
|
||||
import {pasteLinkExtension} from "./editor_paste";
|
||||
import {markdownSyscalls} from "../common/syscalls/markdown";
|
||||
import {clientStoreSyscalls} from "./syscalls/clientStore";
|
||||
import {StatusBar} from "./components/status_bar";
|
||||
import { editorSyscalls } from "./syscalls/editor";
|
||||
import { indexerSyscalls } from "./syscalls/indexer";
|
||||
import { spaceSyscalls } from "./syscalls/space";
|
||||
import { Action, AppViewState, initialViewState } from "./types";
|
||||
import { SilverBulletHooks } from "../common/manifest";
|
||||
import { safeRun, throttle } from "./util";
|
||||
import { System } from "../plugos/system";
|
||||
import { EventHook } from "../plugos/hooks/event";
|
||||
import { systemSyscalls } from "./syscalls/system";
|
||||
import { Panel } from "./components/panel";
|
||||
import { CommandHook } from "./hooks/command";
|
||||
import { SlashCommandHook } from "./hooks/slash_command";
|
||||
import { CompleterHook } from "./hooks/completer";
|
||||
import { pasteLinkExtension } from "./editor_paste";
|
||||
import { markdownSyscalls } from "../common/syscalls/markdown";
|
||||
import { clientStoreSyscalls } from "./syscalls/clientStore";
|
||||
import { StatusBar } from "./components/status_bar";
|
||||
|
||||
class PageState {
|
||||
scrollTop: number;
|
||||
@ -341,6 +341,23 @@ export class Editor implements AppEventDispatcher {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "Ctrl-l",
|
||||
mac: "Cmd-l",
|
||||
run: (): boolean => {
|
||||
this.editorView?.dispatch({
|
||||
effects: [
|
||||
EditorView.scrollIntoView(
|
||||
this.editorView.state.selection.main.anchor,
|
||||
{
|
||||
y: "center",
|
||||
}
|
||||
),
|
||||
],
|
||||
});
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]),
|
||||
|
||||
EditorView.domEventHandlers({
|
||||
@ -409,10 +426,13 @@ export class Editor implements AppEventDispatcher {
|
||||
|
||||
// Persist current page state and nicely close page
|
||||
if (this.currentPage) {
|
||||
let pageState = this.openPages.get(this.currentPage)!;
|
||||
let pageState = this.openPages.get(this.currentPage);
|
||||
if (pageState) {
|
||||
pageState.selection = this.editorView!.state.selection;
|
||||
pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
|
||||
pageState.scrollTop =
|
||||
this.editorView!.scrollDOM.parentElement!.parentElement!.scrollTop;
|
||||
// pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
|
||||
// console.log("Saved pageState", this.currentPage, pageState);
|
||||
}
|
||||
this.space.unwatchPage(this.currentPage);
|
||||
await this.save(true);
|
||||
@ -431,11 +451,12 @@ export class Editor implements AppEventDispatcher {
|
||||
});
|
||||
} else {
|
||||
// Restore state
|
||||
console.log("Restoring selection state", pageState.selection);
|
||||
// console.log("Restoring selection state", pageState);
|
||||
editorView.dispatch({
|
||||
selection: pageState.selection,
|
||||
});
|
||||
editorView.scrollDOM.scrollTop = pageState!.scrollTop;
|
||||
editorView.scrollDOM.parentElement!.parentElement!.scrollTop =
|
||||
pageState!.scrollTop;
|
||||
}
|
||||
|
||||
this.space.watchPage(pageName);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {safeRun} from "./util";
|
||||
import { safeRun } from "./util";
|
||||
|
||||
function encodePageUrl(name: string): string {
|
||||
return name.replaceAll(" ", "_");
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {styleTags, tags as t} from "@codemirror/highlight";
|
||||
import {BlockContext, LeafBlock, LeafBlockParser, MarkdownConfig, TaskList,} from "@lezer/markdown";
|
||||
import {commonmark, mkLang} from "./markdown/markdown";
|
||||
import { styleTags, tags as t } from "@codemirror/highlight";
|
||||
import { BlockContext, LeafBlock, LeafBlockParser, MarkdownConfig, TaskList } from "@lezer/markdown";
|
||||
import { commonmark, mkLang } from "./markdown/markdown";
|
||||
import * as ct from "./customtags";
|
||||
import {pageLinkRegex} from "./constant";
|
||||
import { pageLinkRegex } from "./constant";
|
||||
|
||||
const pageLinkRegexPrefix = new RegExp(
|
||||
"^" + pageLinkRegex.toString().slice(1, -1)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Action, AppViewState} from "./types";
|
||||
import { Action, AppViewState } from "./types";
|
||||
|
||||
export default function reducer(
|
||||
state: AppViewState,
|
||||
|
@ -1,27 +1,14 @@
|
||||
import { EventEmitter } from "../common/event";
|
||||
import { Manifest } from "../common/manifest";
|
||||
import { safeRun } from "./util";
|
||||
import { Plug } from "../plugos/plug";
|
||||
import { PageMeta } from "../common/types";
|
||||
|
||||
export type SpaceEvents = {
|
||||
pageCreated: (meta: PageMeta) => void;
|
||||
pageChanged: (meta: PageMeta) => void;
|
||||
pageDeleted: (name: string) => void;
|
||||
pageListUpdated: (pages: Set<PageMeta>) => void;
|
||||
plugLoaded: (plugName: string, plug: Manifest) => void;
|
||||
plugUnloaded: (plugName: string) => void;
|
||||
};
|
||||
|
||||
type PlugMeta = {
|
||||
name: string;
|
||||
version: number;
|
||||
};
|
||||
import { EventEmitter } from "../../common/event";
|
||||
import { PageMeta } from "../../common/types";
|
||||
import { safeRun } from "../util";
|
||||
import { Plug } from "../../plugos/plug";
|
||||
import { Manifest } from "../../common/manifest";
|
||||
import { PlugMeta, Space, SpaceEvents } from "./space";
|
||||
|
||||
const pageWatchInterval = 2000;
|
||||
const plugWatchInterval = 5000;
|
||||
|
||||
export class Space extends EventEmitter<SpaceEvents> {
|
||||
export class HttpRestSpace extends EventEmitter<SpaceEvents> implements Space {
|
||||
pageUrl: string;
|
||||
pageMetaCache = new Map<string, PageMeta>();
|
||||
plugMetaCache = new Map<string, PlugMeta>();
|
||||
@ -29,7 +16,6 @@ export class Space extends EventEmitter<SpaceEvents> {
|
||||
saving = false;
|
||||
private plugUrl: string;
|
||||
private initialPageListLoad = true;
|
||||
private initialPlugListLoad = true;
|
||||
|
||||
constructor(url: string) {
|
||||
super();
|
||||
@ -40,11 +26,11 @@ export class Space extends EventEmitter<SpaceEvents> {
|
||||
this.updatePageListAsync();
|
||||
}
|
||||
|
||||
public watchPage(pageName: string) {
|
||||
watchPage(pageName: string) {
|
||||
this.watchedPages.add(pageName);
|
||||
}
|
||||
|
||||
public unwatchPage(pageName: string) {
|
||||
unwatchPage(pageName: string) {
|
||||
this.watchedPages.delete(pageName);
|
||||
}
|
||||
|
||||
@ -114,23 +100,11 @@ export class Space extends EventEmitter<SpaceEvents> {
|
||||
});
|
||||
}
|
||||
|
||||
public async listPages(): Promise<Set<PageMeta>> {
|
||||
// this.updatePageListAsync();
|
||||
async listPages(): Promise<Set<PageMeta>> {
|
||||
return new Set([...this.pageMetaCache.values()]);
|
||||
}
|
||||
|
||||
private responseToMetaCacher(name: string, res: Response): PageMeta {
|
||||
const meta = {
|
||||
name,
|
||||
lastModified: +(res.headers.get("Last-Modified") || "0"),
|
||||
};
|
||||
this.pageMetaCache.set(name, meta);
|
||||
return meta;
|
||||
}
|
||||
|
||||
public async readPage(
|
||||
name: string
|
||||
): Promise<{ text: string; meta: PageMeta }> {
|
||||
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
||||
let res = await fetch(`${this.pageUrl}/${name}`, {
|
||||
method: "GET",
|
||||
});
|
||||
@ -140,11 +114,13 @@ export class Space extends EventEmitter<SpaceEvents> {
|
||||
};
|
||||
}
|
||||
|
||||
public async writePage(
|
||||
async writePage(
|
||||
name: string,
|
||||
text: string,
|
||||
selfUpdate?: boolean
|
||||
selfUpdate?: boolean,
|
||||
withMeta?: PageMeta
|
||||
): Promise<PageMeta> {
|
||||
// TODO: withMeta ignored for now
|
||||
try {
|
||||
this.saving = true;
|
||||
let res = await fetch(`${this.pageUrl}/${name}`, {
|
||||
@ -161,7 +137,7 @@ export class Space extends EventEmitter<SpaceEvents> {
|
||||
}
|
||||
}
|
||||
|
||||
public async deletePage(name: string): Promise<void> {
|
||||
async deletePage(name: string): Promise<void> {
|
||||
let req = await fetch(`${this.pageUrl}/${name}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
@ -173,18 +149,7 @@ export class Space extends EventEmitter<SpaceEvents> {
|
||||
this.emit("pageListUpdated", new Set([...this.pageMetaCache.values()]));
|
||||
}
|
||||
|
||||
private async getPageMeta(name: string): Promise<PageMeta> {
|
||||
let res = await fetch(`${this.pageUrl}/${name}`, {
|
||||
method: "OPTIONS",
|
||||
});
|
||||
return this.responseToMetaCacher(name, res);
|
||||
}
|
||||
|
||||
async remoteSyscall(
|
||||
plug: Plug<any>,
|
||||
name: string,
|
||||
args: any[]
|
||||
): Promise<any> {
|
||||
async proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any> {
|
||||
let req = await fetch(`${this.plugUrl}/${plug.name}/syscall/${name}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -202,7 +167,17 @@ export class Space extends EventEmitter<SpaceEvents> {
|
||||
return await req.json();
|
||||
}
|
||||
|
||||
async remoteInvoke(plug: Plug<any>, name: string, args: any[]): Promise<any> {
|
||||
async invokeFunction(
|
||||
plug: Plug<any>,
|
||||
env: string,
|
||||
name: string,
|
||||
args: any[]
|
||||
): Promise<any> {
|
||||
// Invoke locally
|
||||
if (!env || env === "client") {
|
||||
return plug.invoke(name, args);
|
||||
}
|
||||
// Or dispatch to server
|
||||
let req = await fetch(`${this.plugUrl}/${plug.name}/function/${name}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -220,8 +195,38 @@ export class Space extends EventEmitter<SpaceEvents> {
|
||||
return await req.json();
|
||||
}
|
||||
|
||||
async getPageMeta(name: string): Promise<PageMeta> {
|
||||
let res = await fetch(`${this.pageUrl}/${name}`, {
|
||||
method: "OPTIONS",
|
||||
});
|
||||
return this.responseToMetaCacher(name, res);
|
||||
}
|
||||
|
||||
public async listPlugs(): Promise<PlugMeta[]> {
|
||||
let res = await fetch(`${this.plugUrl}`, {
|
||||
method: "GET",
|
||||
});
|
||||
return (await res.json()) as PlugMeta[];
|
||||
}
|
||||
|
||||
public async loadPlug(name: string): Promise<Manifest> {
|
||||
let res = await fetch(`${this.plugUrl}/${name}`, {
|
||||
method: "GET",
|
||||
});
|
||||
return (await res.json()) as Manifest;
|
||||
}
|
||||
|
||||
private responseToMetaCacher(name: string, res: Response): PageMeta {
|
||||
const meta = {
|
||||
name,
|
||||
lastModified: +(res.headers.get("Last-Modified") || "0"),
|
||||
};
|
||||
this.pageMetaCache.set(name, meta);
|
||||
return meta;
|
||||
}
|
||||
|
||||
private async pollPlugs(): Promise<void> {
|
||||
const newPlugs = await this.loadPlugs();
|
||||
const newPlugs = await this.listPlugs();
|
||||
let deletedPlugs = new Set<string>(this.plugMetaCache.keys());
|
||||
for (const newPlugMeta of newPlugs) {
|
||||
const oldPlugMeta = this.plugMetaCache.get(newPlugMeta.name);
|
||||
@ -247,18 +252,4 @@ export class Space extends EventEmitter<SpaceEvents> {
|
||||
this.emit("plugUnloaded", deletedPlug);
|
||||
}
|
||||
}
|
||||
|
||||
private async loadPlugs(): Promise<PlugMeta[]> {
|
||||
let res = await fetch(`${this.plugUrl}`, {
|
||||
method: "GET",
|
||||
});
|
||||
return (await res.json()) as PlugMeta[];
|
||||
}
|
||||
|
||||
private async loadPlug(name: string): Promise<Manifest> {
|
||||
let res = await fetch(`${this.plugUrl}/${name}`, {
|
||||
method: "GET",
|
||||
});
|
||||
return (await res.json()) as Manifest;
|
||||
}
|
||||
}
|
130
webapp/spaces/indexeddb_space.ts
Normal file
130
webapp/spaces/indexeddb_space.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { PlugMeta, Space, SpaceEvents } from "./space";
|
||||
import { EventEmitter } from "../../common/event";
|
||||
import { PageMeta } from "../../common/types";
|
||||
import Dexie, { Table } from "dexie";
|
||||
import { Plug } from "../../plugos/plug";
|
||||
import { Manifest } from "../../common/manifest";
|
||||
|
||||
type Page = {
|
||||
name: string;
|
||||
text: string;
|
||||
meta: PageMeta;
|
||||
};
|
||||
|
||||
type PlugManifest = {
|
||||
name: string;
|
||||
manifest: Manifest;
|
||||
};
|
||||
|
||||
export class IndexedDBSpace extends EventEmitter<SpaceEvents> implements Space {
|
||||
private pageTable: Table<Page, string>;
|
||||
private plugMetaTable: Table<PlugMeta, string>;
|
||||
private plugManifestTable: Table<PlugManifest, string>;
|
||||
|
||||
constructor(dbName: string) {
|
||||
super();
|
||||
const db = new Dexie(dbName);
|
||||
db.version(1).stores({
|
||||
page: "name",
|
||||
plugMeta: "name",
|
||||
plugManifest: "name",
|
||||
});
|
||||
this.pageTable = db.table("page");
|
||||
this.plugMetaTable = db.table("plugMeta");
|
||||
this.plugManifestTable = db.table("plugManifest");
|
||||
}
|
||||
|
||||
async deletePage(name: string): Promise<void> {
|
||||
this.emit("pageDeleted", name);
|
||||
return this.pageTable.delete(name);
|
||||
}
|
||||
|
||||
async getPageMeta(name: string): Promise<PageMeta> {
|
||||
let entry = await this.pageTable.get(name);
|
||||
if (entry) {
|
||||
return entry.meta;
|
||||
} else {
|
||||
throw Error(`Page not found ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
invokeFunction(
|
||||
plug: Plug<any>,
|
||||
env: string,
|
||||
name: string,
|
||||
args: any[]
|
||||
): Promise<any> {
|
||||
return plug.invoke(name, args);
|
||||
}
|
||||
|
||||
async listPages(): Promise<Set<PageMeta>> {
|
||||
let allPages = await this.pageTable.toArray();
|
||||
let set = new Set(allPages.map((p) => p.meta));
|
||||
this.emit("pageListUpdated", set);
|
||||
return set;
|
||||
}
|
||||
|
||||
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any> {
|
||||
return plug.syscall(name, args);
|
||||
}
|
||||
|
||||
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
||||
let page = await this.pageTable.get(name);
|
||||
if (page) {
|
||||
return page!;
|
||||
} else {
|
||||
return {
|
||||
text: "",
|
||||
meta: {
|
||||
name,
|
||||
lastModified: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async writePage(
|
||||
name: string,
|
||||
text: string,
|
||||
selfUpdate?: boolean,
|
||||
withMeta?: PageMeta
|
||||
): Promise<PageMeta> {
|
||||
let meta = withMeta
|
||||
? withMeta
|
||||
: {
|
||||
name,
|
||||
lastModified: new Date().getTime(),
|
||||
};
|
||||
await this.pageTable.put({
|
||||
name,
|
||||
text,
|
||||
meta,
|
||||
});
|
||||
if (!selfUpdate) {
|
||||
this.emit("pageChanged", meta);
|
||||
}
|
||||
// TODO: add pageCreated
|
||||
return meta;
|
||||
}
|
||||
|
||||
unwatchPage(pageName: string): void {}
|
||||
|
||||
updatePageListAsync(): void {
|
||||
this.listPages();
|
||||
}
|
||||
|
||||
watchPage(pageName: string): void {}
|
||||
|
||||
async listPlugs(): Promise<PlugMeta[]> {
|
||||
return this.plugMetaTable.toArray();
|
||||
}
|
||||
|
||||
async loadPlug(name: string): Promise<Manifest> {
|
||||
let plugManifest = await this.plugManifestTable.get(name);
|
||||
if (plugManifest) {
|
||||
return plugManifest.manifest;
|
||||
} else {
|
||||
throw Error(`Plug not found ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
52
webapp/spaces/space.ts
Normal file
52
webapp/spaces/space.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Manifest } from "../../common/manifest";
|
||||
import { Plug } from "../../plugos/plug";
|
||||
import { PageMeta } from "../../common/types";
|
||||
|
||||
export type SpaceEvents = {
|
||||
pageCreated: (meta: PageMeta) => void;
|
||||
pageChanged: (meta: PageMeta) => void;
|
||||
pageDeleted: (name: string) => void;
|
||||
pageListUpdated: (pages: Set<PageMeta>) => void;
|
||||
plugLoaded: (plugName: string, plug: Manifest) => void;
|
||||
plugUnloaded: (plugName: string) => void;
|
||||
};
|
||||
|
||||
export type PlugMeta = {
|
||||
name: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
export interface Space {
|
||||
// Pages
|
||||
watchPage(pageName: string): void;
|
||||
unwatchPage(pageName: string): void;
|
||||
listPages(): Promise<Set<PageMeta>>;
|
||||
readPage(name: string): Promise<{ text: string; meta: PageMeta }>;
|
||||
getPageMeta(name: string): Promise<PageMeta>;
|
||||
writePage(
|
||||
name: string,
|
||||
text: string,
|
||||
selfUpdate?: boolean,
|
||||
withMeta?: PageMeta
|
||||
): Promise<PageMeta>;
|
||||
deletePage(name: string): Promise<void>;
|
||||
|
||||
// Plugs
|
||||
listPlugs(): Promise<PlugMeta[]>;
|
||||
loadPlug(name: string): Promise<Manifest>;
|
||||
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any>;
|
||||
invokeFunction(
|
||||
plug: Plug<any>,
|
||||
env: string,
|
||||
name: string,
|
||||
args: any[]
|
||||
): Promise<any>;
|
||||
|
||||
// Events
|
||||
on(handlers: Partial<SpaceEvents>): void;
|
||||
off(handlers: Partial<SpaceEvents>): void;
|
||||
emit(eventName: keyof SpaceEvents, ...args: any[]): void;
|
||||
|
||||
// TODO: Get rid of this
|
||||
updatePageListAsync(): void;
|
||||
}
|
62
webapp/spaces/sync.test.ts
Normal file
62
webapp/spaces/sync.test.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { expect, test } from "@jest/globals";
|
||||
import { IndexedDBSpace } from "./indexeddb_space";
|
||||
import { SpaceSync } from "./sync";
|
||||
|
||||
// For testing in node.js
|
||||
require("fake-indexeddb/auto");
|
||||
|
||||
test("Test store", async () => {
|
||||
let primary = new IndexedDBSpace("primary");
|
||||
let secondary = new IndexedDBSpace("secondary");
|
||||
let sync = new SpaceSync(primary, secondary, 0);
|
||||
|
||||
// Write one page to primary
|
||||
await primary.writePage("start", "Hello");
|
||||
expect((await secondary.listPages()).size).toBe(0);
|
||||
await sync.syncPages();
|
||||
expect((await secondary.listPages()).size).toBe(1);
|
||||
expect((await secondary.readPage("start")).text).toBe("Hello");
|
||||
let lastSync = sync.lastSync;
|
||||
|
||||
// Should be a no-op
|
||||
await sync.syncPages();
|
||||
expect(sync.lastSync).toBe(lastSync);
|
||||
|
||||
// Now let's make a change on the secondary
|
||||
await secondary.writePage("start", "Hello!!");
|
||||
await secondary.writePage("test", "Test page");
|
||||
|
||||
// And sync it
|
||||
await sync.syncPages();
|
||||
|
||||
expect((await primary.listPages()).size).toBe(2);
|
||||
expect((await secondary.listPages()).size).toBe(2);
|
||||
|
||||
expect((await primary.readPage("start")).text).toBe("Hello!!");
|
||||
|
||||
// Let's make some random edits on both ends
|
||||
await primary.writePage("start", "1");
|
||||
await primary.writePage("start2", "2");
|
||||
await secondary.writePage("start3", "3");
|
||||
await secondary.writePage("start4", "4");
|
||||
|
||||
await sync.syncPages();
|
||||
|
||||
expect((await primary.listPages()).size).toBe(5);
|
||||
expect((await secondary.listPages()).size).toBe(5);
|
||||
|
||||
console.log("Should be no op");
|
||||
await sync.syncPages();
|
||||
|
||||
console.log("Done");
|
||||
|
||||
// Cause a conflict
|
||||
await primary.writePage("start", "Hello 1");
|
||||
await secondary.writePage("start", "Hello 2");
|
||||
|
||||
try {
|
||||
await sync.syncPages();
|
||||
// This should throw a sync conflict, so cannot be here
|
||||
expect(false).toBe(true);
|
||||
} catch {}
|
||||
});
|
99
webapp/spaces/sync.ts
Normal file
99
webapp/spaces/sync.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { Space } from "./space";
|
||||
|
||||
export class SpaceSync {
|
||||
lastSync: number;
|
||||
|
||||
constructor(
|
||||
private primary: Space,
|
||||
private secondary: Space,
|
||||
lastSync: number
|
||||
) {
|
||||
this.lastSync = lastSync;
|
||||
}
|
||||
|
||||
async syncPages() {
|
||||
let allPagesPrimary = new Map(
|
||||
[...(await this.primary.listPages())].map((p) => [p.name, p])
|
||||
);
|
||||
let allPagesSecondary = new Map(
|
||||
[...(await this.secondary.listPages())].map((p) => [p.name, p])
|
||||
);
|
||||
|
||||
let createdPagesOnSecondary = new Set<string>();
|
||||
|
||||
// Iterate over all pages on the primary first
|
||||
for (let [name, pageMetaPrimary] of allPagesPrimary.entries()) {
|
||||
let pageMetaSecondary = allPagesSecondary.get(pageMetaPrimary.name);
|
||||
if (!pageMetaSecondary) {
|
||||
// New page on primary
|
||||
// Push from primary to secondary
|
||||
console.log("New page on primary", name, "syncing to secondary");
|
||||
let pageData = await this.primary.readPage(name);
|
||||
await this.secondary.writePage(
|
||||
name,
|
||||
pageData.text,
|
||||
true,
|
||||
pageData.meta
|
||||
);
|
||||
createdPagesOnSecondary.add(name);
|
||||
} else {
|
||||
// Existing page
|
||||
if (pageMetaPrimary.lastModified > this.lastSync) {
|
||||
// Primary updated since last sync
|
||||
if (pageMetaSecondary.lastModified > this.lastSync) {
|
||||
// Secondary also updated! CONFLICT
|
||||
throw Error(`Sync conflict for ${name}`);
|
||||
} else {
|
||||
// Ok, not changed on secondary, push it secondary
|
||||
console.log(
|
||||
"Changed page on primary",
|
||||
name,
|
||||
"syncing to secondary"
|
||||
);
|
||||
let pageData = await this.primary.readPage(name);
|
||||
await this.secondary.writePage(
|
||||
name,
|
||||
pageData.text,
|
||||
true,
|
||||
pageData.meta
|
||||
);
|
||||
}
|
||||
} else if (pageMetaSecondary.lastModified > this.lastSync) {
|
||||
// Secondary updated, but not primary (checked above)
|
||||
// Push from secondary to primary
|
||||
console.log("Changed page on secondary", name, "syncing to primary");
|
||||
let pageData = await this.secondary.readPage(name);
|
||||
await this.primary.writePage(
|
||||
name,
|
||||
pageData.text,
|
||||
true,
|
||||
pageData.meta
|
||||
);
|
||||
} else {
|
||||
// Neither updated, no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now do a simplified version in reverse, only detecting new pages
|
||||
|
||||
// Finally, let's go over all pages on the secondary and see if the primary has them
|
||||
for (let [name, pageMetaSecondary] of allPagesSecondary.entries()) {
|
||||
if (!allPagesPrimary.has(pageMetaSecondary.name)) {
|
||||
// New page on secondary
|
||||
// Push from secondary to primary
|
||||
console.log("New page on secondary", name, "pushing to primary");
|
||||
let pageData = await this.secondary.readPage(name);
|
||||
await this.primary.writePage(name, pageData.text, true, pageData.meta);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the latest timestamp on the primary and set it as lastSync
|
||||
allPagesPrimary.forEach((pageMeta) => {
|
||||
this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
|
||||
});
|
||||
allPagesSecondary.forEach((pageMeta) => {
|
||||
this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import {transportSyscalls} from "../../plugos/syscalls/transport";
|
||||
import {SysCallMapping} from "../../plugos/system";
|
||||
import {storeSyscalls} from "../../plugos/syscalls/store.dexie_browser";
|
||||
import { proxySyscalls } from "../../plugos/syscalls/transport";
|
||||
import { SysCallMapping } from "../../plugos/system";
|
||||
import { storeSyscalls } from "../../plugos/syscalls/store.dexie_browser";
|
||||
|
||||
export function clientStoreSyscalls(): SysCallMapping {
|
||||
const storeCalls = storeSyscalls("local", "localData");
|
||||
return transportSyscalls(
|
||||
return proxySyscalls(
|
||||
["clientStore.get", "clientStore.set", "clientStore.delete"],
|
||||
(ctx, name, ...args) => {
|
||||
return storeCalls[name.replace("clientStore.", "store.")](ctx, ...args);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Editor} from "../editor";
|
||||
import {Transaction} from "@codemirror/state";
|
||||
import {SysCallMapping} from "../../plugos/system";
|
||||
import { Editor } from "../editor";
|
||||
import { Transaction } from "@codemirror/state";
|
||||
import { SysCallMapping } from "../../plugos/system";
|
||||
|
||||
type SyntaxNode = {
|
||||
name: string;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {Space} from "../space";
|
||||
import {SysCallMapping} from "../../plugos/system";
|
||||
import {transportSyscalls} from "../../plugos/syscalls/transport";
|
||||
import { Space } from "../spaces/space";
|
||||
import { SysCallMapping } from "../../plugos/system";
|
||||
import { proxySyscalls } from "../../plugos/syscalls/transport";
|
||||
|
||||
export function indexerSyscalls(space: Space): SysCallMapping {
|
||||
return transportSyscalls(
|
||||
return proxySyscalls(
|
||||
[
|
||||
"index.scanPrefixForPage",
|
||||
"index.scanPrefixGlobal",
|
||||
@ -12,6 +12,6 @@ export function indexerSyscalls(space: Space): SysCallMapping {
|
||||
"index.batchSet",
|
||||
"index.delete",
|
||||
],
|
||||
(ctx, name, ...args) => space.remoteSyscall(ctx.plug, name, args)
|
||||
(ctx, name, ...args) => space.proxySyscall(ctx.plug, name, args)
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Editor} from "../editor";
|
||||
import {SysCallMapping} from "../../plugos/system";
|
||||
import {PageMeta} from "../../common/types";
|
||||
import { Editor } from "../editor";
|
||||
import { SysCallMapping } from "../../plugos/system";
|
||||
import { PageMeta } from "../../common/types";
|
||||
|
||||
export function spaceSyscalls(editor: Editor): SysCallMapping {
|
||||
return {
|
||||
|
@ -1,17 +1,19 @@
|
||||
import {SysCallMapping} from "../../plugos/system";
|
||||
import {Space} from "../space";
|
||||
import { SysCallMapping } from "../../plugos/system";
|
||||
import { Space } from "../spaces/space";
|
||||
|
||||
export function systemSyscalls(space: Space): SysCallMapping {
|
||||
return {
|
||||
"system.invokeFunctionOnServer": async (
|
||||
"system.invokeFunction": async (
|
||||
ctx,
|
||||
env: string,
|
||||
name: string,
|
||||
...args: any[]
|
||||
) => {
|
||||
if (!ctx.plug) {
|
||||
throw Error("No plug associated with context");
|
||||
}
|
||||
return space.remoteInvoke(ctx.plug, name, args);
|
||||
|
||||
return space.invokeFunction(ctx.plug, env, name, args);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {AppCommand} from "./hooks/command";
|
||||
import {PageMeta} from "../common/types";
|
||||
import { AppCommand } from "./hooks/command";
|
||||
import { PageMeta } from "../common/types";
|
||||
|
||||
export const slashCommandRegexp = /\/[\w\-]*/;
|
||||
|
||||
|
60
yarn.lock
60
yarn.lock
@ -1883,6 +1883,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.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
|
||||
@ -2287,6 +2292,11 @@ cookiejar@^2.1.3:
|
||||
resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz"
|
||||
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.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
|
||||
@ -2567,6 +2577,11 @@ detect-newline@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz"
|
||||
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
|
||||
|
||||
dexie@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.1.tgz#ef21456d725e700c1ab7ac4307896e4fdabaf753"
|
||||
integrity sha512-Y8oz3t2XC9hvjkP35B5I8rUkKKwM36GGRjWQCMjzIYScg7W+GHKDXobSYswkisW7CxL1/tKQtggMDsiWqDUc1g==
|
||||
|
||||
dezalgo@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz"
|
||||
@ -2594,6 +2609,13 @@ domelementtype@^2.0.1, domelementtype@^2.2.0:
|
||||
resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz"
|
||||
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.npmjs.org/domexception/-/domexception-2.0.1.tgz"
|
||||
@ -2866,6 +2888,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.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
|
||||
@ -5158,6 +5187,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.npmjs.org/rechoir/-/rechoir-0.8.0.tgz"
|
||||
@ -5788,6 +5827,20 @@ typescript@^4.6.2:
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz"
|
||||
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==
|
||||
|
||||
unbox-primitive@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz"
|
||||
@ -5937,6 +5990,11 @@ webidl-conversions@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
|
||||
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
|
||||
|
||||
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.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz"
|
||||
@ -5967,7 +6025,7 @@ whatwg-url@^5.0.0:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
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==
|
||||
|
Loading…
Reference in New Issue
Block a user