Tons of work

This commit is contained in:
Zef Hemel
2022-03-14 10:07:38 +01:00
parent da4bf4a9ab
commit 1984d8eefe
27 changed files with 3644 additions and 690 deletions
+207 -208
View File
@@ -11,220 +11,219 @@ import { PageMeta } from "./server";
import { ClientPageState, Page } from "./types";
import { safeRun } from "./util";
export class SocketAPI {
openPages = new Map<string, Page>();
connectedSockets: Set<Socket> = new Set();
pageStore: DiskStorage;
export function exposeSocketAPI(rootPath: string, io: Server) {
const openPages = new Map<string, Page>();
const connectedSockets: Set<Socket> = new Set();
const pageStore = new DiskStorage(rootPath);
fileWatcher(rootPath);
constructor(rootPath: string, io: Server) {
this.pageStore = new DiskStorage(rootPath);
this.fileWatcher(rootPath);
io.on("connection", (socket) => {
const socketOpenPages = new Set<string>();
io.on("connection", (socket) => {
console.log("Connected", socket.id);
this.connectedSockets.add(socket);
const socketOpenPages = new Set<string>();
console.log("Connected", socket.id);
connectedSockets.add(socket);
socket.on("disconnect", () => {
console.log("Disconnected", socket.id);
socketOpenPages.forEach(disconnectPageSocket);
this.connectedSockets.delete(socket);
});
function onCall(eventName: string, cb: (...args: any[]) => Promise<any>) {
socket.on(eventName, (reqId: number, ...args) => {
cb(...args).then((result) => {
socket.emit(`${eventName}Resp${reqId}`, result);
});
});
}
const _this = this;
function disconnectPageSocket(pageName: string) {
let page = _this.openPages.get(pageName);
if (page) {
for (let client of page.clientStates) {
if (client.socket === socket) {
_this.disconnectClient(client, page);
}
}
}
}
onCall("openPage", async (pageName: string) => {
let page = this.openPages.get(pageName);
if (!page) {
try {
let { text, meta } = await this.pageStore.readPage(pageName);
page = new Page(pageName, text, meta);
} catch (e) {
console.log("Creating new page", pageName);
page = new Page(pageName, "", { name: pageName, lastModified: 0 });
}
this.openPages.set(pageName, page);
}
page.clientStates.add(new ClientPageState(socket, page.version));
socketOpenPages.add(pageName);
console.log("Opened page", pageName);
this.broadcastCursors(page);
return page.toJSON();
});
socket.on("closePage", (pageName: string) => {
console.log("Closing page", pageName);
socketOpenPages.delete(pageName);
disconnectPageSocket(pageName);
});
onCall(
"pushUpdates",
async (
pageName: string,
version: number,
updates: any[]
): Promise<boolean> => {
let page = this.openPages.get(pageName);
if (!page) {
console.error(
"Received updates for not open page",
pageName,
this.openPages.keys()
);
return false;
}
if (version !== page.version) {
console.error("Invalid version", version, page.version);
return false;
} else {
console.log("Applying", updates.length, "updates to", pageName);
let transformedUpdates = [];
let textChanged = false;
for (let update of updates) {
let changes = ChangeSet.fromJSON(update.changes);
let transformedUpdate = {
changes,
clientID: update.clientID,
effects: update.cursors?.map((c: Cursor) => {
page.cursors.set(c.userId, c);
return cursorEffect.of(c);
}),
};
page.updates.push(transformedUpdate);
transformedUpdates.push(transformedUpdate);
let oldText = page.text;
page.text = changes.apply(page.text);
if (oldText !== page.text) {
textChanged = true;
}
}
if (textChanged) {
if (page.saveTimer) {
clearTimeout(page.saveTimer);
}
page.saveTimer = setTimeout(() => {
this.flushPageToDisk(pageName, page);
}, 1000);
}
while (page.pending.length) {
page.pending.pop()!(transformedUpdates);
}
return true;
}
}
);
onCall(
"pullUpdates",
async (pageName: string, version: number): Promise<Update[]> => {
let page = this.openPages.get(pageName);
// console.log("Pulling updates for", pageName);
if (!page) {
console.error("Fetching updates for not open page");
return [];
}
// TODO: Optimize this
let oldestVersion = Infinity;
page.clientStates.forEach((client) => {
oldestVersion = Math.min(client.version, oldestVersion);
if (client.socket === socket) {
client.version = version;
}
});
page.flushUpdates(oldestVersion);
if (version < page.version) {
return page.updatesSince(version);
} else {
return new Promise((resolve) => {
page.pending.push(resolve);
});
}
}
);
onCall(
"readPage",
async (pageName: string): Promise<{ text: string; meta: PageMeta }> => {
let page = this.openPages.get(pageName);
if (page) {
console.log("Serving page from memory", pageName);
return {
text: page.text.sliceString(0),
meta: page.meta,
};
} else {
return this.pageStore.readPage(pageName);
}
}
);
onCall("writePage", async (pageName: string, text: string) => {
let page = this.openPages.get(pageName);
if (page) {
for (let client of page.clientStates) {
client.socket.emit("reloadPage", pageName);
}
this.openPages.delete(pageName);
}
return this.pageStore.writePage(pageName, text);
});
onCall("deletePage", async (pageName: string) => {
this.openPages.delete(pageName);
socketOpenPages.delete(pageName);
// Cascading of this to all connected clients will be handled by file watcher
return this.pageStore.deletePage(pageName);
});
onCall("listPages", async (): Promise<PageMeta[]> => {
return this.pageStore.listPages();
});
onCall("getPageMeta", async (pageName: string): Promise<PageMeta> => {
let page = this.openPages.get(pageName);
if (page) {
return page.meta;
}
return this.pageStore.getPageMeta(pageName);
});
socket.on("disconnect", () => {
console.log("Disconnected", socket.id);
socketOpenPages.forEach(disconnectPageSocket);
connectedSockets.delete(socket);
});
}
private disconnectClient(client: ClientPageState, page: Page) {
socket.on("closePage", (pageName: string) => {
console.log("Closing page", pageName);
socketOpenPages.delete(pageName);
disconnectPageSocket(pageName);
});
const onCall = (
eventName: string,
cb: (...args: any[]) => Promise<any>
) => {
socket.on(eventName, (reqId: number, ...args) => {
cb(...args).then((result) => {
socket.emit(`${eventName}Resp${reqId}`, result);
});
});
};
const disconnectPageSocket = (pageName: string) => {
let page = openPages.get(pageName);
if (page) {
for (let client of page.clientStates) {
if (client.socket === socket) {
disconnectClient(client, page);
}
}
}
};
onCall("openPage", async (pageName: string) => {
let page = openPages.get(pageName);
if (!page) {
try {
let { text, meta } = await pageStore.readPage(pageName);
page = new Page(pageName, text, meta);
} catch (e) {
console.log("Creating new page", pageName);
page = new Page(pageName, "", { name: pageName, lastModified: 0 });
}
openPages.set(pageName, page);
}
page.clientStates.add(new ClientPageState(socket, page.version));
socketOpenPages.add(pageName);
console.log("Opened page", pageName);
broadcastCursors(page);
return page.toJSON();
});
onCall(
"pushUpdates",
async (
pageName: string,
version: number,
updates: any[]
): Promise<boolean> => {
let page = openPages.get(pageName);
if (!page) {
console.error(
"Received updates for not open page",
pageName,
openPages.keys()
);
return false;
}
if (version !== page.version) {
console.error("Invalid version", version, page.version);
return false;
} else {
console.log("Applying", updates.length, "updates to", pageName);
let transformedUpdates = [];
let textChanged = false;
for (let update of updates) {
let changes = ChangeSet.fromJSON(update.changes);
let transformedUpdate = {
changes,
clientID: update.clientID,
effects: update.cursors?.map((c: Cursor) => {
page.cursors.set(c.userId, c);
return cursorEffect.of(c);
}),
};
page.updates.push(transformedUpdate);
transformedUpdates.push(transformedUpdate);
let oldText = page.text;
page.text = changes.apply(page.text);
if (oldText !== page.text) {
textChanged = true;
}
}
if (textChanged) {
if (page.saveTimer) {
clearTimeout(page.saveTimer);
}
page.saveTimer = setTimeout(() => {
flushPageToDisk(pageName, page);
}, 1000);
}
while (page.pending.length) {
page.pending.pop()!(transformedUpdates);
}
return true;
}
}
);
onCall(
"pullUpdates",
async (pageName: string, version: number): Promise<Update[]> => {
let page = openPages.get(pageName);
// console.log("Pulling updates for", pageName);
if (!page) {
console.error("Fetching updates for not open page");
return [];
}
// TODO: Optimize this
let oldestVersion = Infinity;
page.clientStates.forEach((client) => {
oldestVersion = Math.min(client.version, oldestVersion);
if (client.socket === socket) {
client.version = version;
}
});
page.flushUpdates(oldestVersion);
if (version < page.version) {
return page.updatesSince(version);
} else {
return new Promise((resolve) => {
page.pending.push(resolve);
});
}
}
);
onCall(
"readPage",
async (pageName: string): Promise<{ text: string; meta: PageMeta }> => {
let page = openPages.get(pageName);
if (page) {
console.log("Serving page from memory", pageName);
return {
text: page.text.sliceString(0),
meta: page.meta,
};
} else {
return pageStore.readPage(pageName);
}
}
);
onCall("writePage", async (pageName: string, text: string) => {
let page = openPages.get(pageName);
if (page) {
for (let client of page.clientStates) {
client.socket.emit("reloadPage", pageName);
}
openPages.delete(pageName);
}
return pageStore.writePage(pageName, text);
});
onCall("deletePage", async (pageName: string) => {
openPages.delete(pageName);
socketOpenPages.delete(pageName);
// Cascading of this to all connected clients will be handled by file watcher
return pageStore.deletePage(pageName);
});
onCall("listPages", async (): Promise<PageMeta[]> => {
return pageStore.listPages();
});
onCall("getPageMeta", async (pageName: string): Promise<PageMeta> => {
let page = openPages.get(pageName);
if (page) {
return page.meta;
}
return pageStore.getPageMeta(pageName);
});
});
function disconnectClient(client: ClientPageState, page: Page) {
page.clientStates.delete(client);
if (page.clientStates.size === 0) {
console.log("No more clients for", page.name, "flushing");
this.flushPageToDisk(page.name, page);
this.openPages.delete(page.name);
flushPageToDisk(page.name, page);
openPages.delete(page.name);
} else {
page.cursors.delete(client.socket.id);
this.broadcastCursors(page);
broadcastCursors(page);
}
}
private broadcastCursors(page: Page) {
function broadcastCursors(page: Page) {
page.clientStates.forEach((client) => {
client.socket.emit(
"cursorSnapshot",
@@ -234,15 +233,15 @@ export class SocketAPI {
});
}
private flushPageToDisk(name: string, page: Page) {
function flushPageToDisk(name: string, page: Page) {
safeRun(async () => {
let meta = await this.pageStore.writePage(name, page.text.sliceString(0));
let meta = await pageStore.writePage(name, page.text.sliceString(0));
console.log(`Wrote page ${name} to disk`);
page.meta = meta;
});
}
private fileWatcher(rootPath: string) {
function fileWatcher(rootPath: string) {
fs.watch(
rootPath,
{
@@ -264,16 +263,16 @@ export class SocketAPI {
} catch (e) {
// File was deleted
console.log("Deleted", pageName);
for (let socket of this.connectedSockets) {
for (let socket of connectedSockets) {
socket.emit("pageDeleted", pageName);
}
return;
}
const openPage = this.openPages.get(pageName);
const openPage = openPages.get(pageName);
if (openPage) {
if (openPage.meta.lastModified < modifiedTime) {
console.log("Page changed on disk outside of editor, reloading");
this.openPages.delete(pageName);
openPages.delete(pageName);
const meta = {
name: pageName,
lastModified: modifiedTime,
@@ -288,7 +287,7 @@ export class SocketAPI {
console.log(
"New file created, broadcasting to all connected sockets"
);
for (let socket of this.connectedSockets) {
for (let socket of connectedSockets) {
socket.emit("pageCreated", {
name: pageName,
lastModified: modifiedTime,
+3 -5
View File
@@ -12,9 +12,7 @@ export class DiskStorage {
async listPages(): Promise<PageMeta[]> {
let fileNames: PageMeta[] = [];
let _this = this;
async function walkPath(dir: string) {
const walkPath = async (dir: string) => {
let files = await readdir(dir);
for (let file of files) {
const fullPath = path.join(dir, file);
@@ -25,7 +23,7 @@ export class DiskStorage {
if (path.extname(file) === ".md") {
fileNames.push({
name: fullPath.substring(
_this.rootPath.length + 1,
this.rootPath.length + 1,
fullPath.length - 3
),
lastModified: s.mtime.getTime(),
@@ -33,7 +31,7 @@ export class DiskStorage {
}
}
}
}
};
await walkPath(this.rootPath);
return fileNames;
}
+2 -2
View File
@@ -2,7 +2,7 @@ import express from "express";
import { readFile } from "fs/promises";
import http from "http";
import { Server } from "socket.io";
import { SocketAPI } from "./api";
import { exposeSocketAPI } from "./api";
const app = express();
const server = http.createServer(app);
@@ -24,7 +24,7 @@ export type PageMeta = {
};
app.use("/", express.static(distDir));
let filesystem = new SocketAPI(pagesPath, io);
exposeSocketAPI(pagesPath, io);
// Fallback, serve index.html
let cachedIndex: string | undefined = undefined;