Now all communication happens over sockets

This commit is contained in:
Zef Hemel
2022-03-11 11:49:42 +01:00
parent 5c5e232034
commit da4bf4a9ab
20 changed files with 467 additions and 430 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
import { Editor } from "./editor";
import { HttpRemoteSpace } from "./space";
import { RealtimeSpace } from "./space";
import { safeRun } from "./util";
import { io } from "socket.io-client";
let socket = io(`http://${location.hostname}:3000`);
let editor = new Editor(
new HttpRemoteSpace(`http://${location.hostname}:3000/fs`, socket),
new RealtimeSpace(socket),
document.getElementById("root")!
);
+20 -22
View File
@@ -10,8 +10,8 @@ import {
receiveUpdates,
sendableUpdates,
} from "@codemirror/collab";
import { RangeSetBuilder, Range } from "@codemirror/rangeset";
import { EditorState, StateEffect, StateField, Text } from "@codemirror/state";
import { RangeSetBuilder } from "@codemirror/rangeset";
import { Text } from "@codemirror/state";
import {
Decoration,
DecorationSet,
@@ -21,7 +21,7 @@ import {
WidgetType,
} from "@codemirror/view";
import { Cursor, cursorEffect } from "./cursorEffect";
import { HttpRemoteSpace } from "./space";
import { RealtimeSpace, SpaceEventHandlers } from "./space";
const throttleInterval = 250;
@@ -85,7 +85,7 @@ export function collabExtension(
pageName: string,
clientID: string,
doc: Document,
space: HttpRemoteSpace,
space: RealtimeSpace,
reloadCallback: () => void
) {
let plugin = ViewPlugin.fromClass(
@@ -95,7 +95,15 @@ export function collabExtension(
private failedPushes = 0;
decorations: DecorationSet;
private cursorPositions: Map<string, Cursor> = doc.cursors;
throttledPush: () => void;
throttledPush = throttle(() => this.push(), throttleInterval);
eventHandlers: Partial<SpaceEventHandlers> = {
cursorSnapshot: (pageName, cursors) => {
console.log("Received new cursor snapshot", cursors);
this.cursorPositions = new Map(Object.entries(cursors));
},
};
buildDecorations(view: EditorView) {
let builder = new RangeSetBuilder<Decoration>();
@@ -128,18 +136,7 @@ export function collabExtension(
this.pull();
}
this.decorations = this.buildDecorations(view);
this.throttledPush = throttle(() => this.push(), throttleInterval);
console.log("Created collabo plug");
space.addEventListener("cursors", this.updateCursors);
}
updateCursors(cursorEvent: any) {
this.cursorPositions = new Map();
console.log("Received new cursor snapshot", cursorEvent.detail, this);
for (let userId in cursorEvent.detail) {
this.cursorPositions.set(userId, cursorEvent.detail[userId]);
}
space.on(this.eventHandlers);
}
update(update: ViewUpdate) {
@@ -190,7 +187,7 @@ export function collabExtension(
let success = await space.pushUpdates(pageName, version, updates);
this.pushing = false;
if (!success) {
if (!success && !this.done) {
this.failedPushes++;
if (this.failedPushes > 10) {
// Not sure if 10 is a good number, but YOLO
@@ -198,14 +195,16 @@ export function collabExtension(
reloadCallback();
return this.destroy();
}
console.log("Push failed temporarily, but will try again");
console.log(
`Push for page ${pageName} failed temporarily, but will try again`
);
} else {
this.failedPushes = 0;
}
// Regardless of whether the push failed or new updates came in
// while it was running, try again if there's updates remaining
if (sendableUpdates(this.view.state).length) {
if (!this.done && sendableUpdates(this.view.state).length) {
// setTimeout(() => this.push(), 100);
this.throttledPush();
}
@@ -236,7 +235,7 @@ export function collabExtension(
destroy() {
this.done = true;
space.removeEventListener("cursors", this.updateCursors);
space.off(this.eventHandlers);
}
},
{
@@ -252,7 +251,6 @@ export function collabExtension(
return tr.effects.filter((e) => e.is(cursorEffect));
},
}),
// cursorField,
plugin,
];
}
+1 -1
View File
@@ -100,7 +100,7 @@ export function FilterList({
ref={searchBoxRef}
onChange={filter}
onKeyDown={(e: React.KeyboardEvent) => {
console.log("Key up", e.key);
// console.log("Key up", e.key);
if (onKeyPress) {
onKeyPress(e.key, text);
}
+3 -3
View File
@@ -7,7 +7,7 @@ export function PageNavigator({
onNavigate,
currentPage,
}: {
allPages: PageMeta[];
allPages: Set<PageMeta>;
onNavigate: (page: string | undefined) => void;
currentPage?: string;
}) {
@@ -17,10 +17,10 @@ export function PageNavigator({
continue;
}
// Order by last modified date in descending order
let orderId = -pageMeta.lastModified.getTime();
let orderId = -pageMeta.lastModified;
// Unless it was opened and is still in memory
if (pageMeta.lastOpened) {
orderId = -pageMeta.lastOpened.getTime();
orderId = -pageMeta.lastOpened;
}
options.push({
...pageMeta,
+27 -27
View File
@@ -45,7 +45,7 @@ import { slashCommandRegexp } from "./types";
import reducer from "./reducer";
import { smartQuoteKeymap } from "./smart_quotes";
import { HttpRemoteSpace } from "./space";
import { RealtimeSpace } from "./space";
import customMarkdownStyle from "./style";
import dbSyscalls from "./syscalls/db.localstorage";
import editorSyscalls from "./syscalls/editor.browser";
@@ -84,7 +84,7 @@ export class Editor implements AppEventDispatcher {
viewState: AppViewState;
viewDispatch: React.Dispatch<Action>;
openPages: Map<string, PageState>;
space: HttpRemoteSpace;
space: RealtimeSpace;
editorCommands: Map<string, AppCommand>;
plugs: Plug<NuggetHook>[];
indexer: Indexer;
@@ -92,7 +92,7 @@ export class Editor implements AppEventDispatcher {
pageNavigator: IPageNavigator;
indexCurrentPageDebounced: () => any;
constructor(space: HttpRemoteSpace, parent: Element) {
constructor(space: RealtimeSpace, parent: Element) {
this.editorCommands = new Map();
this.openPages = new Map();
this.plugs = [];
@@ -114,7 +114,7 @@ export class Editor implements AppEventDispatcher {
}
async init() {
await this.loadPageList();
// await this.loadPageList();
await this.loadPlugs();
this.focus();
@@ -127,8 +127,10 @@ export class Editor implements AppEventDispatcher {
if (this.currentPage) {
let pageState = this.openPages.get(this.currentPage)!;
pageState.selection = this.editorView!.state.selection;
pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
if (pageState) {
pageState.selection = this.editorView!.state.selection;
pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
}
this.space.closePage(this.currentPage);
}
@@ -136,19 +138,25 @@ export class Editor implements AppEventDispatcher {
await this.loadPage(pageName);
});
this.space.addEventListener("connect", () => {
if (this.currentPage) {
console.log("Connected to socket, fetch fresh?");
this.reloadPage();
}
});
this.space.addEventListener("reload", (e) => {
let pageName = (e as CustomEvent).detail;
if (this.currentPage === pageName) {
console.log("Was told to reload the page");
this.reloadPage();
}
this.space.on({
connect: () => {
if (this.currentPage) {
console.log("Connected to socket, fetch fresh?");
this.reloadPage();
}
},
pageChanged: (meta) => {
if (this.currentPage === meta.name) {
console.log("page changed on disk, reloading");
this.reloadPage();
}
},
pageListUpdated: (pages) => {
this.viewDispatch({
type: "pages-listed",
pages: pages,
});
},
});
if (this.pageNavigator.getCurrentPage() === "") {
@@ -411,14 +419,6 @@ export class Editor implements AppEventDispatcher {
}
}
async loadPageList() {
let pagesMeta = await this.space.listPages();
this.viewDispatch({
type: "pages-listed",
pages: pagesMeta,
});
}
focus() {
this.editorView!.focus();
}
+6 -4
View File
@@ -9,10 +9,12 @@ export default function reducer(
case "page-loaded":
return {
...state,
allPages: state.allPages.map((pageMeta) =>
pageMeta.name === action.name
? { ...pageMeta, lastOpened: new Date() }
: pageMeta
allPages: new Set(
[...state.allPages].map((pageMeta) =>
pageMeta.name === action.name
? { ...pageMeta, lastOpened: Date.now() }
: pageMeta
)
),
currentPage: action.name,
};
+72 -54
View File
@@ -14,27 +14,81 @@ export interface Space {
getPageMeta(name: string): Promise<PageMeta>;
}
export class HttpRemoteSpace extends EventTarget implements Space {
url: string;
export type SpaceEventHandlers = {
connect: () => void;
cursorSnapshot: (
pageName: string,
cursors: { [key: string]: Cursor }
) => void;
pageCreated: (meta: PageMeta) => void;
pageChanged: (meta: PageMeta) => void;
pageDeleted: (name: string) => void;
pageListUpdated: (pages: Set<PageMeta>) => void;
};
abstract class EventEmitter<HandlerT> {
private handlers: Partial<HandlerT>[] = [];
on(handlers: Partial<HandlerT>) {
this.handlers.push(handlers);
}
off(handlers: Partial<HandlerT>) {
this.handlers = this.handlers.filter((h) => h !== handlers);
}
emit(eventName: keyof HandlerT, ...args: any[]) {
for (let handler of this.handlers) {
let fn: any = handler[eventName];
if (fn) {
fn(...args);
}
}
}
}
export class RealtimeSpace
extends EventEmitter<SpaceEventHandlers>
implements Space
{
socket: Socket;
reqId = 0;
allPages = new Set<PageMeta>();
constructor(url: string, socket: Socket) {
constructor(socket: Socket) {
super();
this.url = url;
this.socket = socket;
socket.on("connect", () => {
console.log("connected to socket");
this.dispatchEvent(new Event("connect"));
[
"connect",
"cursorSnapshot",
"pageCreated",
"pageChanged",
"pageDeleted",
].forEach((eventName) => {
socket.on(eventName, (...args) => {
this.emit(eventName as keyof SpaceEventHandlers, ...args);
});
});
socket.on("reload", (pageName: string) => {
this.dispatchEvent(new CustomEvent("reload", { detail: pageName }));
this.wsCall("listPages").then((pages) => {
this.allPages = new Set(pages);
this.emit("pageListUpdated", this.allPages);
});
socket.on("cursors", (cursors) => {
this.dispatchEvent(new CustomEvent("cursors", { detail: cursors }));
this.on({
pageCreated: (meta) => {
this.allPages.add(meta);
console.log("New page created", meta);
this.emit("pageListUpdated", this.allPages);
},
pageDeleted: (name) => {
console.log("Page delete", name);
this.allPages.forEach((meta) => {
if (name === meta.name) {
this.allPages.delete(meta);
}
});
this.emit("pageListUpdated", this.allPages);
},
});
}
@@ -76,14 +130,7 @@ export class HttpRemoteSpace extends EventTarget implements Space {
}
async listPages(): Promise<PageMeta[]> {
let req = await fetch(this.url, {
method: "GET",
});
return (await req.json()).map((meta: any) => ({
name: meta.name,
lastModified: new Date(meta.lastModified),
}));
return Array.from(this.allPages);
}
async openPage(name: string): Promise<Document> {
@@ -101,47 +148,18 @@ export class HttpRemoteSpace extends EventTarget implements Space {
}
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
let req = await fetch(`${this.url}/${name}`, {
method: "GET",
});
return {
text: await req.text(),
meta: {
lastModified: new Date(+req.headers.get("Last-Modified")!),
name: name,
},
};
return this.wsCall("readPage", name);
}
async writePage(name: string, text: string): Promise<PageMeta> {
let req = await fetch(`${this.url}/${name}`, {
method: "PUT",
body: text,
});
// 201 (Created) means a new page was created
return {
lastModified: new Date(+req.headers.get("Last-Modified")!),
name: name,
created: req.status === 201,
};
return this.wsCall("writePage", name, text);
}
async deletePage(name: string): Promise<void> {
let req = await fetch(`${this.url}/${name}`, {
method: "DELETE",
});
if (req.status !== 200) {
throw Error(`Failed to delete page: ${req.statusText}`);
}
return this.wsCall("deletePage", name);
}
async getPageMeta(name: string): Promise<PageMeta> {
let req = await fetch(`${this.url}/${name}`, {
method: "OPTIONS",
});
return {
name: name,
lastModified: new Date(+req.headers.get("Last-Modified")!),
};
return this.wsCall("deletePage", name);
}
}
+1 -4
View File
@@ -3,10 +3,7 @@ import { PageMeta } from "../types";
export default (editor: Editor) => ({
"space.listPages": (): PageMeta[] => {
return editor.viewState.allPages;
},
"space.reloadPageList": async () => {
await editor.loadPageList();
return [...editor.viewState.allPages];
},
"space.reindex": async () => {
await editor.indexer.reindexSpace(editor.space, editor);
+5 -6
View File
@@ -10,10 +10,9 @@ export type Manifest = plugbox.Manifest<NuggetHook>;
export type PageMeta = {
name: string;
lastModified: Date;
lastModified: number;
version?: number;
created?: boolean;
lastOpened?: Date;
lastOpened?: number;
};
export type AppCommand = {
@@ -40,20 +39,20 @@ export type AppViewState = {
currentPage?: string;
showPageNavigator: boolean;
showCommandPalette: boolean;
allPages: PageMeta[];
allPages: Set<PageMeta>;
commands: Map<string, AppCommand>;
};
export const initialViewState: AppViewState = {
showPageNavigator: false,
showCommandPalette: false,
allPages: [],
allPages: new Set(),
commands: new Map(),
};
export type Action =
| { type: "page-loaded"; name: string }
| { type: "pages-listed"; pages: PageMeta[] }
| { type: "pages-listed"; pages: Set<PageMeta> }
| { type: "start-navigate" }
| { type: "stop-navigate" }
| { type: "update-commands"; commands: Map<string, AppCommand> }