Now all communication happens over sockets
This commit is contained in:
+2
-2
@@ -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
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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> }
|
||||
|
||||
Reference in New Issue
Block a user