Work on sync stuff
This commit is contained in:
+6
-2
@@ -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} />
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+61
-40
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import {safeRun} from "./util";
|
||||
import { safeRun } from "./util";
|
||||
|
||||
function encodePageUrl(name: string): string {
|
||||
return name.replaceAll(" ", "_");
|
||||
|
||||
+4
-4
@@ -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
-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;
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+2
-2
@@ -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\-]*/;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user