Collab work
parent
ef7bc0beae
commit
b5a8cd7d1b
|
@ -8,4 +8,5 @@ website_build
|
||||||
.idea
|
.idea
|
||||||
deno.lock
|
deno.lock
|
||||||
fly.toml
|
fly.toml
|
||||||
env.sh
|
env.sh
|
||||||
|
node_modules
|
|
@ -13,12 +13,23 @@ await esbuild.build({
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
minify: false,
|
minify: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
|
// ESBuild plugin to make npm modules external
|
||||||
|
{
|
||||||
|
name: "npm-external",
|
||||||
|
setup(build: any) {
|
||||||
|
build.onResolve({ filter: /^npm:/ }, (args: any) => {
|
||||||
|
return {
|
||||||
|
path: args.path,
|
||||||
|
external: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "json",
|
name: "json",
|
||||||
setup: (build) =>
|
setup: (build) =>
|
||||||
build.onLoad({ filter: /\.json$/ }, () => ({ loader: "json" })),
|
build.onLoad({ filter: /\.json$/ }, () => ({ loader: "json" })),
|
||||||
},
|
},
|
||||||
|
|
||||||
...denoPlugins({
|
...denoPlugins({
|
||||||
importMapURL: new URL("./import_map.json", import.meta.url)
|
importMapURL: new URL("./import_map.json", import.meta.url)
|
||||||
.toString(),
|
.toString(),
|
||||||
|
|
|
@ -7,3 +7,7 @@ export function start(serverUrl: string, token: string, username: string) {
|
||||||
export function stop() {
|
export function stop() {
|
||||||
return syscall("collab.stop");
|
return syscall("collab.stop");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ping(clientId: string, currentPage: string) {
|
||||||
|
return syscall("collab.ping", clientId, currentPage);
|
||||||
|
}
|
||||||
|
|
|
@ -110,6 +110,7 @@ export async function detectPage() {
|
||||||
console.error("Error parsing YAML", e);
|
console.error("Error parsing YAML", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await ping();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shareNoop() {
|
export function shareNoop() {
|
||||||
|
@ -160,3 +161,48 @@ export function writeFileCollab(name: string): FileMeta {
|
||||||
perm: "rw",
|
perm: "rw",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clientId = nanoid();
|
||||||
|
let currentCollabId: string | undefined;
|
||||||
|
|
||||||
|
const localCollabServer = location.protocol === "http:"
|
||||||
|
? `ws://${location.host}/.ws-collab`
|
||||||
|
: `wss://${location.host}/.ws-collab`;
|
||||||
|
|
||||||
|
async function ping() {
|
||||||
|
try {
|
||||||
|
const currentPage = await editor.getCurrentPage();
|
||||||
|
const { collabId } = await collab.ping(
|
||||||
|
clientId,
|
||||||
|
currentPage,
|
||||||
|
);
|
||||||
|
console.log("Collab ID", collabId);
|
||||||
|
if (!collabId && currentCollabId) {
|
||||||
|
// Stop collab
|
||||||
|
console.log("Stopping collab");
|
||||||
|
// editor.flashNotification("Closing real-time collaboration mode.");
|
||||||
|
currentCollabId = undefined;
|
||||||
|
await collab.stop();
|
||||||
|
} else if (collabId && collabId !== currentCollabId) {
|
||||||
|
// Start collab
|
||||||
|
console.log("Starting collab");
|
||||||
|
editor.flashNotification("Opening page in real-time collaboration mode.");
|
||||||
|
currentCollabId = collabId;
|
||||||
|
await collab.start(
|
||||||
|
localCollabServer,
|
||||||
|
`${collabId}/${currentPage}`,
|
||||||
|
"you",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
// console.error("Ping error", e);
|
||||||
|
if (e.message.includes("Failed to fetch") && currentCollabId) {
|
||||||
|
console.log("Offline, stopping collab");
|
||||||
|
currentCollabId = undefined;
|
||||||
|
await collab.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(() => {
|
||||||
|
ping().catch(console.error);
|
||||||
|
}, 5000);
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { assert, assertEquals } from "../test_deps.ts";
|
||||||
|
import { CollabServer } from "./collab.ts";
|
||||||
|
|
||||||
|
Deno.test("Collab server", async () => {
|
||||||
|
const collabServer = new CollabServer(null as any);
|
||||||
|
console.log("Client 1 joins page 1");
|
||||||
|
assertEquals(collabServer.ping("client1", "page1"), {});
|
||||||
|
assertEquals(collabServer.clients.size, 1);
|
||||||
|
assertEquals(collabServer.pages.size, 1);
|
||||||
|
console.log("Client 1 joins page 2");
|
||||||
|
assertEquals(collabServer.ping("client1", "page2"), {});
|
||||||
|
assertEquals(collabServer.clients.size, 1);
|
||||||
|
assertEquals(collabServer.pages.size, 1);
|
||||||
|
console.log("Client 2 joins to page 2, collab id created");
|
||||||
|
const collabId = collabServer.ping("client2", "page2").collabId;
|
||||||
|
assertEquals(collabServer.clients.size, 2);
|
||||||
|
assert(collabId !== undefined);
|
||||||
|
console.log("Client 2 moves to page 1, collab id destroyed");
|
||||||
|
assertEquals(collabServer.ping("client2", "page1"), {});
|
||||||
|
assertEquals(collabServer.ping("client1", "page2"), {});
|
||||||
|
console.log("Going to cleanup, which should have no effect");
|
||||||
|
collabServer.cleanup(50);
|
||||||
|
assertEquals(collabServer.clients.size, 2);
|
||||||
|
collabServer.ping("client2", "page2");
|
||||||
|
console.log("Going to sleep 20ms");
|
||||||
|
await sleep(20);
|
||||||
|
console.log("Then client 1 pings, but client 2 does not");
|
||||||
|
assertEquals(collabServer.ping("client1", "page2"), {});
|
||||||
|
await sleep(20);
|
||||||
|
console.log("Going to cleanup, which should clean client 2");
|
||||||
|
collabServer.cleanup(35);
|
||||||
|
assertEquals(collabServer.clients.size, 1);
|
||||||
|
assertEquals(collabServer.pages.get("page2")!.collabId, undefined);
|
||||||
|
console.log(collabServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
import { Hocuspocus } from "npm:@hocuspocus/server@2.0.6";
|
||||||
|
import { getAvailablePortSync } from "https://deno.land/x/port@1.0.0/mod.ts";
|
||||||
|
import { nanoid } from "https://esm.sh/nanoid@4.0.0";
|
||||||
|
import { race, timeout } from "../common/async_util.ts";
|
||||||
|
import { Application } from "./deps.ts";
|
||||||
|
import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
|
||||||
|
|
||||||
|
type CollabPage = {
|
||||||
|
clients: Set<string>; // clientIds
|
||||||
|
collabId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pingInterval = 5000;
|
||||||
|
|
||||||
|
export class CollabServer {
|
||||||
|
clients: Map<string, { openPage: string; lastPing: number }> = new Map();
|
||||||
|
pages: Map<string, CollabPage> = new Map();
|
||||||
|
|
||||||
|
constructor(private spacePrimitives: SpacePrimitives) {
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
setInterval(() => {
|
||||||
|
this.cleanup(3 * pingInterval);
|
||||||
|
}, pingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
ping(clientId: string, currentPage: string): { collabId?: string } {
|
||||||
|
let clientState = this.clients.get(clientId);
|
||||||
|
let collabId: string | undefined;
|
||||||
|
if (!clientState) {
|
||||||
|
clientState = {
|
||||||
|
openPage: "",
|
||||||
|
lastPing: Date.now(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
clientState.lastPing = Date.now();
|
||||||
|
}
|
||||||
|
if (currentPage !== clientState.openPage) {
|
||||||
|
// Client switched pages
|
||||||
|
// Update last page record
|
||||||
|
const lastCollabPage = this.pages.get(clientState.openPage);
|
||||||
|
if (lastCollabPage) {
|
||||||
|
lastCollabPage.clients.delete(clientId);
|
||||||
|
if (lastCollabPage.clients.size === 0) {
|
||||||
|
// Cleanup
|
||||||
|
this.pages.delete(clientState.openPage);
|
||||||
|
} else {
|
||||||
|
if (lastCollabPage.clients.size === 1) {
|
||||||
|
delete lastCollabPage.collabId;
|
||||||
|
}
|
||||||
|
this.pages.set(clientState.openPage, lastCollabPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update new page
|
||||||
|
let nextCollabPage = this.pages.get(currentPage);
|
||||||
|
if (!nextCollabPage) {
|
||||||
|
nextCollabPage = {
|
||||||
|
clients: new Set(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
nextCollabPage.clients.add(clientId);
|
||||||
|
if (nextCollabPage.clients.size === 2) {
|
||||||
|
// Create a new collabId
|
||||||
|
nextCollabPage.collabId = nanoid();
|
||||||
|
}
|
||||||
|
clientState.openPage = currentPage;
|
||||||
|
this.pages.set(currentPage, nextCollabPage);
|
||||||
|
collabId = nextCollabPage.collabId;
|
||||||
|
} else {
|
||||||
|
// Page didn't change
|
||||||
|
collabId = this.pages.get(currentPage)?.collabId;
|
||||||
|
}
|
||||||
|
this.clients.set(clientId, clientState);
|
||||||
|
if (collabId) {
|
||||||
|
return { collabId };
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup(timeout: number) {
|
||||||
|
// Clean up clients that haven't pinged for some time
|
||||||
|
for (const [clientId, clientState] of this.clients) {
|
||||||
|
if (Date.now() - clientState.lastPing > timeout) {
|
||||||
|
console.log("[Collab]", "Ejecting client", clientId);
|
||||||
|
this.clients.delete(clientId);
|
||||||
|
const collabPage = this.pages.get(clientState.openPage);
|
||||||
|
if (collabPage) {
|
||||||
|
collabPage.clients.delete(clientId);
|
||||||
|
if (collabPage.clients.size === 0) {
|
||||||
|
this.pages.delete(clientState.openPage);
|
||||||
|
} else {
|
||||||
|
if (collabPage.clients.size === 1) {
|
||||||
|
delete collabPage.collabId;
|
||||||
|
}
|
||||||
|
this.pages.set(clientState.openPage, collabPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
route(app: Application) {
|
||||||
|
// The way this works is that we spin up a separate WS server locally and then proxy requests to it
|
||||||
|
// This is the only way I could get Hocuspocus to work with Deno
|
||||||
|
const internalPort = getAvailablePortSync();
|
||||||
|
const hocuspocus = new Hocuspocus({
|
||||||
|
port: internalPort,
|
||||||
|
address: "localhost",
|
||||||
|
quiet: true,
|
||||||
|
onLoadDocument: async (doc) => {
|
||||||
|
console.log("[Hocuspocus]", "Requesting doc load", doc.documentName);
|
||||||
|
const pageName = doc.documentName.split("/").slice(1).join("/");
|
||||||
|
try {
|
||||||
|
const yText = doc.document.getText("codemirror");
|
||||||
|
const { data } = await this.spacePrimitives.readFile(
|
||||||
|
`${pageName}.md`,
|
||||||
|
);
|
||||||
|
|
||||||
|
yText.insert(0, new TextDecoder().decode(data));
|
||||||
|
console.log("[Hocuspocus]", "Loaded document from space");
|
||||||
|
return doc.document;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error loading doc", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDisconnect: (client) => {
|
||||||
|
console.log("[Hocuspocus]", "Client disconnected", client.clientsCount);
|
||||||
|
if (client.clientsCount === 0) {
|
||||||
|
console.log(
|
||||||
|
"[Hocuspocus]",
|
||||||
|
"Last client disconnected from",
|
||||||
|
client.documentName,
|
||||||
|
"purging from memory",
|
||||||
|
);
|
||||||
|
hocuspocus.documents.delete(client.documentName);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
hocuspocus.listen();
|
||||||
|
app.use((ctx) => {
|
||||||
|
if (ctx.request.url.pathname === "/.ws") {
|
||||||
|
const sock = ctx.upgrade();
|
||||||
|
sock.onmessage = (e) => {
|
||||||
|
console.log("WS: Got message", e.data);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Websocket proxy to hocuspocus
|
||||||
|
if (ctx.request.url.pathname === "/.ws-collab") {
|
||||||
|
const sock = ctx.upgrade();
|
||||||
|
|
||||||
|
const ws = new WebSocket(`ws://localhost:${internalPort}`);
|
||||||
|
const wsReady = race([
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
ws.onopen = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
timeout(1000),
|
||||||
|
]).catch(() => {
|
||||||
|
console.error("Timeout waiting for collab to open websocket");
|
||||||
|
sock.close();
|
||||||
|
});
|
||||||
|
sock.onmessage = (e) => {
|
||||||
|
// console.log("Got message", e);
|
||||||
|
wsReady.then(() => ws.send(e.data)).catch(console.error);
|
||||||
|
};
|
||||||
|
sock.onclose = () => {
|
||||||
|
if (ws.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
if (sock.OPEN) {
|
||||||
|
sock.send(e.data);
|
||||||
|
} else {
|
||||||
|
console.error("Got message from websocket but socket is not open");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
if (sock.OPEN) {
|
||||||
|
sock.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import { performLocalFetch } from "../common/proxy_fetch.ts";
|
||||||
import { BuiltinSettings } from "../web/types.ts";
|
import { BuiltinSettings } from "../web/types.ts";
|
||||||
import { gitIgnoreCompiler } from "./deps.ts";
|
import { gitIgnoreCompiler } from "./deps.ts";
|
||||||
import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts";
|
import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts";
|
||||||
|
import { CollabServer } from "./collab.ts";
|
||||||
|
|
||||||
export type ServerOptions = {
|
export type ServerOptions = {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
@ -29,6 +30,7 @@ export class HttpServer {
|
||||||
clientAssetBundle: AssetBundle;
|
clientAssetBundle: AssetBundle;
|
||||||
settings?: BuiltinSettings;
|
settings?: BuiltinSettings;
|
||||||
spacePrimitives: SpacePrimitives;
|
spacePrimitives: SpacePrimitives;
|
||||||
|
collab: CollabServer;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
spacePrimitives: SpacePrimitives,
|
spacePrimitives: SpacePrimitives,
|
||||||
|
@ -62,6 +64,8 @@ export class HttpServer {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
this.collab = new CollabServer(this.spacePrimitives);
|
||||||
|
this.collab.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replaces some template variables in index.html in a rather ad-hoc manner, but YOLO
|
// Replaces some template variables in index.html in a rather ad-hoc manner, but YOLO
|
||||||
|
@ -123,7 +127,8 @@ export class HttpServer {
|
||||||
this.app.use(({ request, response }, next) => {
|
this.app.use(({ request, response }, next) => {
|
||||||
if (
|
if (
|
||||||
!request.url.pathname.startsWith("/.fs") &&
|
!request.url.pathname.startsWith("/.fs") &&
|
||||||
request.url.pathname !== "/.auth"
|
request.url.pathname !== "/.auth" &&
|
||||||
|
!request.url.pathname.startsWith("/.ws")
|
||||||
) {
|
) {
|
||||||
response.headers.set("Content-type", "text/html");
|
response.headers.set("Content-type", "text/html");
|
||||||
response.body = this.renderIndexHtml();
|
response.body = this.renderIndexHtml();
|
||||||
|
@ -138,6 +143,8 @@ export class HttpServer {
|
||||||
this.app.use(fsRouter.routes());
|
this.app.use(fsRouter.routes());
|
||||||
this.app.use(fsRouter.allowedMethods());
|
this.app.use(fsRouter.allowedMethods());
|
||||||
|
|
||||||
|
this.collab.route(this.app);
|
||||||
|
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
const listenOptions: any = {
|
const listenOptions: any = {
|
||||||
hostname: this.hostname,
|
hostname: this.hostname,
|
||||||
|
@ -273,6 +280,15 @@ export class HttpServer {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
case "ping": {
|
||||||
|
response.headers.set("Content-Type", "application/json");
|
||||||
|
// console.log("Got ping", body);
|
||||||
|
response.body = JSON.stringify(
|
||||||
|
this.collab.ping(body.clientId, body.page),
|
||||||
|
);
|
||||||
|
// console.log(this.collab);
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
response.headers.set("Content-Type", "text/plain");
|
response.headers.set("Content-Type", "text/plain");
|
||||||
response.status = 400;
|
response.status = 400;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Extension, WebsocketProvider, Y, yCollab } from "../deps.ts";
|
import { Extension, HocuspocusProvider, Y, yCollab } from "../deps.ts";
|
||||||
|
|
||||||
const userColors = [
|
const userColors = [
|
||||||
{ color: "#30bced", light: "#30bced33" },
|
{ color: "#30bced", light: "#30bced33" },
|
||||||
|
@ -12,27 +12,27 @@ const userColors = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export class CollabState {
|
export class CollabState {
|
||||||
ydoc: Y.Doc;
|
public ytext: Y.Text;
|
||||||
collabProvider: WebsocketProvider;
|
private collabProvider: HocuspocusProvider;
|
||||||
ytext: Y.Text;
|
private yundoManager: Y.UndoManager;
|
||||||
yundoManager: Y.UndoManager;
|
|
||||||
|
|
||||||
constructor(serverUrl: string, token: string, username: string) {
|
constructor(serverUrl: string, name: string, username: string) {
|
||||||
this.ydoc = new Y.Doc();
|
this.collabProvider = new HocuspocusProvider({
|
||||||
this.collabProvider = new WebsocketProvider(
|
url: serverUrl,
|
||||||
serverUrl,
|
name: name,
|
||||||
token,
|
});
|
||||||
this.ydoc,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.collabProvider.on("status", (e: any) => {
|
this.collabProvider.on("status", (e: any) => {
|
||||||
console.log("Collab status change", e);
|
console.log("Collab status change", e);
|
||||||
});
|
});
|
||||||
this.collabProvider.on("sync", (e: any) => {
|
// this.collabProvider.on("sync", (e: any) => {
|
||||||
console.log("Sync status", e);
|
// console.log("Sync status", e);
|
||||||
});
|
// });
|
||||||
|
// this.collabProvider.on("synced", (e: any) => {
|
||||||
|
// console.log("Synced status", e);
|
||||||
|
// });
|
||||||
|
|
||||||
this.ytext = this.ydoc.getText("codemirror");
|
this.ytext = this.collabProvider.document.getText("codemirror");
|
||||||
this.yundoManager = new Y.UndoManager(this.ytext);
|
this.yundoManager = new Y.UndoManager(this.ytext);
|
||||||
|
|
||||||
const randomColor =
|
const randomColor =
|
||||||
|
@ -46,6 +46,7 @@ export class CollabState {
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
this.collabProvider.disconnect();
|
||||||
this.collabProvider.destroy();
|
this.collabProvider.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ export {
|
||||||
yCollab,
|
yCollab,
|
||||||
yUndoManagerKeymap,
|
yUndoManagerKeymap,
|
||||||
} from "https://esm.sh/y-codemirror.next@0.3.2?external=yjs,@codemirror/state,@codemirror/commands,@codemirror/history,@codemirror/view";
|
} from "https://esm.sh/y-codemirror.next@0.3.2?external=yjs,@codemirror/state,@codemirror/commands,@codemirror/history,@codemirror/view";
|
||||||
export { WebsocketProvider } from "https://esm.sh/y-websocket@1.4.5?external=yjs";
|
export { HocuspocusProvider } from "https://esm.sh/@hocuspocus/provider@2.0.6?external=yjs,ws";
|
||||||
|
|
||||||
// Vim mode
|
// Vim mode
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -389,7 +389,8 @@ export class Editor {
|
||||||
|
|
||||||
this.space.on({
|
this.space.on({
|
||||||
pageChanged: (meta) => {
|
pageChanged: (meta) => {
|
||||||
if (this.currentPage === meta.name) {
|
// Only reload when watching the current page (to avoid reloading when switching pages and in collab mode)
|
||||||
|
if (this.space.watchInterval && this.currentPage === meta.name) {
|
||||||
console.log("Page changed elsewhere, reloading");
|
console.log("Page changed elsewhere, reloading");
|
||||||
this.flashNotification("Page changed elsewhere, reloading");
|
this.flashNotification("Page changed elsewhere, reloading");
|
||||||
this.reloadPage();
|
this.reloadPage();
|
||||||
|
@ -1144,8 +1145,7 @@ export class Editor {
|
||||||
await this.save(true);
|
await this.save(true);
|
||||||
// And stop the collab session
|
// And stop the collab session
|
||||||
if (this.collabState) {
|
if (this.collabState) {
|
||||||
this.collabState.stop();
|
this.stopCollab();
|
||||||
this.collabState = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1226,10 +1226,18 @@ export class Editor {
|
||||||
if (pageState) {
|
if (pageState) {
|
||||||
// Restore state
|
// Restore state
|
||||||
editorView.scrollDOM.scrollTop = pageState!.scrollTop;
|
editorView.scrollDOM.scrollTop = pageState!.scrollTop;
|
||||||
editorView.dispatch({
|
try {
|
||||||
selection: pageState.selection,
|
editorView.dispatch({
|
||||||
scrollIntoView: true,
|
selection: pageState.selection,
|
||||||
});
|
scrollIntoView: true,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// This is fine, just go to the top
|
||||||
|
editorView.dispatch({
|
||||||
|
selection: { anchor: 0 },
|
||||||
|
scrollIntoView: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
editorView.scrollDOM.scrollTop = 0;
|
editorView.scrollDOM.scrollTop = 0;
|
||||||
editorView.dispatch({
|
editorView.dispatch({
|
||||||
|
@ -1508,12 +1516,24 @@ export class Editor {
|
||||||
}
|
}
|
||||||
const initialText = this.editorView!.state.sliceDoc();
|
const initialText = this.editorView!.state.sliceDoc();
|
||||||
this.collabState = new CollabState(serverUrl, token, username);
|
this.collabState = new CollabState(serverUrl, token, username);
|
||||||
this.collabState.collabProvider.once("sync", (synced: boolean) => {
|
// this.collabState.collabProvider.on("synced", () => {
|
||||||
if (this.collabState?.ytext.toString() === "") {
|
// if (this.collabState?.ytext.toString() === "") {
|
||||||
console.log("Synced value is empty, putting back original text");
|
// console.error("Synced value is empty, putting back original text");
|
||||||
this.collabState?.ytext.insert(0, initialText);
|
// this.collabState?.ytext.insert(0, initialText);
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
this.rebuildEditorState();
|
this.rebuildEditorState();
|
||||||
|
// Don't watch for local changes in this mode
|
||||||
|
this.space.unwatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopCollab() {
|
||||||
|
if (this.collabState) {
|
||||||
|
this.collabState.stop();
|
||||||
|
this.collabState = undefined;
|
||||||
|
this.rebuildEditorState();
|
||||||
|
}
|
||||||
|
// Start file watching again
|
||||||
|
this.space.watch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ export class Space extends EventEmitter<SpaceEvents> {
|
||||||
super();
|
super();
|
||||||
this.kvStore.get("imageHeightCache").then((cache) => {
|
this.kvStore.get("imageHeightCache").then((cache) => {
|
||||||
if (cache) {
|
if (cache) {
|
||||||
console.log("Loaded image height cache from KV store", cache);
|
// console.log("Loaded image height cache from KV store", cache);
|
||||||
this.imageHeightCache = cache;
|
this.imageHeightCache = cache;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,25 @@ export function collabSyscalls(editor: Editor): SysCallMapping {
|
||||||
"collab.stop": (
|
"collab.stop": (
|
||||||
_ctx,
|
_ctx,
|
||||||
) => {
|
) => {
|
||||||
editor.collabState?.stop();
|
editor.stopCollab();
|
||||||
|
},
|
||||||
|
"collab.ping": async (
|
||||||
|
_ctx,
|
||||||
|
clientId: string,
|
||||||
|
currentPage: string,
|
||||||
|
) => {
|
||||||
|
const resp = await editor.remoteSpacePrimitives.authenticatedFetch(
|
||||||
|
editor.remoteSpacePrimitives.url,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
operation: "ping",
|
||||||
|
clientId,
|
||||||
|
page: currentPage,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return resp.json();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue