No More Collab. Fixes #449

* Fully removes real-time collaboration
* URL scheme rewrite
This commit is contained in:
Zef Hemel
2023-07-06 16:47:50 +02:00
committed by GitHub
parent 021bf7e9a4
commit fb75ea1a65
45 changed files with 380 additions and 1356 deletions
-99
View File
@@ -1,99 +0,0 @@
import { safeRun } from "../../common/util.ts";
import { Extension, HocuspocusProvider, Y, yCollab } from "../deps.ts";
import { SyncService } from "../sync_service.ts";
const userColors = [
{ color: "#30bced", light: "#30bced33" },
{ color: "#6eeb83", light: "#6eeb8333" },
{ color: "#ffbc42", light: "#ffbc4233" },
{ color: "#ecd444", light: "#ecd44433" },
{ color: "#ee6352", light: "#ee635233" },
{ color: "#9ac2c9", light: "#9ac2c933" },
{ color: "#8acb88", light: "#8acb8833" },
{ color: "#1be7ff", light: "#1be7ff33" },
];
export class CollabState {
public ytext: Y.Text;
collabProvider: HocuspocusProvider;
private yundoManager: Y.UndoManager;
interval?: number;
constructor(
serverUrl: string,
readonly path: string,
readonly token: string,
username: string,
private syncService: SyncService,
public isLocalCollab: boolean,
) {
this.collabProvider = new HocuspocusProvider({
url: serverUrl,
name: token,
// Receive broadcasted messages from the server (right now only "page has been persisted" notifications)
onStateless: (
{ payload },
) => {
const message = JSON.parse(payload);
switch (message.type) {
case "persisted": {
// Received remote persist notification, updating snapshot
syncService.updateRemoteLastModified(
message.path,
message.lastModified,
).catch(console.error);
}
}
},
});
this.collabProvider.on("status", (e: any) => {
console.log("Collab status change", e);
});
this.ytext = this.collabProvider.document.getText("codemirror");
this.yundoManager = new Y.UndoManager(this.ytext);
const randomColor =
userColors[Math.floor(Math.random() * userColors.length)];
this.collabProvider.awareness.setLocalStateField("user", {
name: username,
color: randomColor.color,
colorLight: randomColor.light,
});
if (isLocalCollab) {
syncService.excludeFromSync(path).catch(console.error);
this.interval = setInterval(() => {
// Ping the store to make sure the file remains in exclusion
syncService.excludeFromSync(path).catch(console.error);
}, 1000);
}
}
stop() {
console.log("[COLLAB] Destroying collab provider");
if (this.interval) {
clearInterval(this.interval);
}
this.collabProvider.destroy();
// For whatever reason, destroy() doesn't properly clean up everything so we need to help a bit
this.collabProvider.configuration.websocketProvider.webSocket = null;
this.collabProvider.configuration.websocketProvider.destroy();
// When stopping collaboration, we're going back to sync mode. Make sure we got the latest and greatest remote timestamp to avoid
// conflicts
safeRun(async () => {
await this.syncService.unExcludeFromSync(this.path);
await this.syncService.fetchAndPersistRemoteLastModified(this.path);
});
}
collabExtension(): Extension {
return yCollab(this.ytext, this.collabProvider.awareness, {
undoManager: this.yundoManager,
});
}
}
-1
View File
@@ -21,7 +21,6 @@ class IFrameWidget extends WidgetType {
}
toDOM(): HTMLElement {
console.log("toDOM");
const iframe = document.createElement("iframe");
iframe.srcdoc = panelHtml;
// iframe.style.height = "0";
+1 -1
View File
@@ -74,7 +74,7 @@ export function inlineImagesPlugin(editor: Editor) {
let url = imageRexexResult.groups.url;
const title = imageRexexResult.groups.title;
if (url.indexOf("://") === -1) {
url = `/.fs/${url}`;
url = decodeURI(url);
}
widgets.push(
Decoration.widget({
+1 -1
View File
@@ -39,7 +39,7 @@ class TableViewWidget extends WidgetType {
annotationPositions: true,
translateUrls: (url) => {
if (!url.includes("://")) {
return `/.fs/${url}`;
return `/${url}`;
}
return url;
},
-83
View File
@@ -1,83 +0,0 @@
import { nanoid } from "https://esm.sh/nanoid@4.0.0";
import type { Editor } from "./editor.tsx";
const collabPingInterval = 2500;
export class CollabManager {
clientId = nanoid();
localCollabServer: string;
constructor(private editor: Editor) {
this.localCollabServer = location.protocol === "http:"
? `ws://${location.host}/.ws-collab`
: `wss://${location.host}/.ws-collab`;
editor.eventHook.addLocalListener(
"editor:pageLoaded",
(pageName, previousPage) => {
console.log("Page loaded", pageName, previousPage);
this.updatePresence(pageName).catch(console.error);
},
);
}
start() {
setInterval(() => {
this.updatePresence(this.editor.currentPage!).catch(console.error);
}, collabPingInterval);
}
async updatePresence(currentPage: string) {
try {
// This is signaled through an OPTIONS call on the file we have open
const resp = await this.editor.remoteSpacePrimitives.authenticatedFetch(
`${this.editor.remoteSpacePrimitives.url}/${currentPage}.md`,
{
method: "OPTIONS",
headers: {
"X-Client-Id": this.clientId,
},
},
);
const collabId = resp.headers.get("X-Collab-Id");
// Not reading body at all, is that a problem?
if (this.editor.collabState && !this.editor.collabState.isLocalCollab) {
// We're in a remote collab mode, don't do anything
return;
}
// console.log("Collab ID", collabId);
const previousCollabId = this.editor.collabState?.token.split("/")[0];
if (!collabId && this.editor.collabState) {
// Stop collab
console.log("Stopping collab");
if (this.editor.collabState.path === `${currentPage}.md`) {
this.editor.flashNotification(
"Other users have left this page, switched back to single-user mode.",
);
}
this.editor.stopCollab();
} else if (collabId && collabId !== previousCollabId) {
// Start collab
console.log("Starting collab");
this.editor.flashNotification(
"Opening page in multi-user mode.",
);
this.editor.startCollab(
this.localCollabServer,
`${collabId}/${currentPage}.md`,
this.editor.getUsername(),
true,
);
}
} catch (e: any) {
// console.error("Ping error", e);
if (
e.message.toLowerCase().includes("failed") && this.editor.collabState
) {
console.log("Offline, stopping collab");
this.editor.stopCollab();
}
}
}
}
-8
View File
@@ -15,14 +15,6 @@ export {
Terminal as TerminalIcon,
} from "https://esm.sh/preact-feather@4.2.1?external=preact";
// Y collab
export * as Y from "yjs";
export {
yCollab,
yUndoManagerKeymap,
} from "https://esm.sh/y-codemirror.next@0.3.2?external=yjs,@codemirror/state,@codemirror/commands,@codemirror/history,@codemirror/view";
export { HocuspocusProvider } from "https://esm.sh/@hocuspocus/provider@2.2.0?deps=lib0@0.2.70&external=yjs,ws&target=es2022";
// Vim mode
export {
getCM as vimGetCm,
+17 -73
View File
@@ -68,7 +68,6 @@ import assetSyscalls from "../plugos/syscalls/asset.ts";
import { eventSyscalls } from "../plugos/syscalls/event.ts";
import { System } from "../plugos/system.ts";
import { cleanModePlugins } from "./cm_plugins/clean.ts";
import { CollabState } from "./cm_plugins/collab.ts";
import {
attachmentExtension,
pasteLinkExtension,
@@ -91,14 +90,12 @@ import {
useEffect,
useReducer,
vim,
yUndoManagerKeymap,
} from "./deps.ts";
import { AppCommand, CommandHook } from "./hooks/command.ts";
import { SlashCommandHook } from "./hooks/slash_command.ts";
import { PathPageNavigator } from "./navigator.ts";
import reducer from "./reducer.ts";
import customMarkdownStyle from "./style.ts";
import { collabSyscalls } from "./syscalls/collab.ts";
import { editorSyscalls } from "./syscalls/editor.ts";
import { spaceSyscalls } from "./syscalls/space.ts";
import { systemSyscalls } from "./syscalls/system.ts";
@@ -137,7 +134,8 @@ import { HttpSpacePrimitives } from "../common/spaces/http_space_primitives.ts";
import { FallbackSpacePrimitives } from "../common/spaces/fallback_space_primitives.ts";
import { syncSyscalls } from "./syscalls/sync.ts";
import { FilteredSpacePrimitives } from "../common/spaces/filtered_space_primitives.ts";
import { CollabManager } from "./collab_manager.ts";
import { run } from "../plug-api/plugos-syscall/shell.ts";
import { isValidPageName } from "$sb/lib/page.ts";
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
@@ -155,7 +153,6 @@ declare global {
// Injected via index.html
silverBulletConfig: {
spaceFolderPath: string;
syncEndpoint: string;
};
editor: Editor;
}
@@ -190,11 +187,9 @@ export class Editor {
fullSyncCompleted = false;
// Runtime state (that doesn't make sense in viewState)
collabState?: CollabState;
syncService: SyncService;
settings?: BuiltinSettings;
kvStore: DexieKVStore;
collabManager: CollabManager;
constructor(
parent: Element,
@@ -216,8 +211,6 @@ export class Editor {
this.eventHook = new EventHook();
system.addHook(this.eventHook);
this.collabManager = new CollabManager(this);
// Cron hook
const cronHook = new CronHook(system);
system.addHook(cronHook);
@@ -237,7 +230,7 @@ export class Editor {
// Setup space
this.remoteSpacePrimitives = new HttpSpacePrimitives(
runtimeConfig.syncEndpoint,
location.origin,
runtimeConfig.spaceFolderPath,
true,
);
@@ -328,7 +321,6 @@ export class Editor {
systemSyscalls(this, this.system),
markdownSyscalls(buildMarkdown(this.mdExtensions)),
assetSyscalls(this.system),
collabSyscalls(this),
yamlSyscalls(),
storeCalls,
indexSyscalls,
@@ -376,17 +368,12 @@ export class Editor {
}
});
// globalThis.addEventListener("beforeunload", (e) => {
// console.log("Pinging with with undefined page name");
// this.collabManager.updatePresence(undefined, this.currentPage);
// });
this.eventHook.addLocalListener("plug:changed", async (fileName) => {
console.log("Plug updated, reloading:", fileName);
system.unload(fileName);
await system.load(
// await this.space.readFile(fileName, "utf8"),
new URL(`/.fs/${fileName}`, location.href),
new URL(`/${fileName}`, location.href),
createSandbox,
);
this.plugsUpdated = true;
@@ -402,7 +389,7 @@ export class Editor {
this.space.on({
pageChanged: (meta) => {
// Only reload when watching the current page (to avoid reloading when switching pages and in collab mode)
// Only reload when watching the current page (to avoid reloading when switching pages)
if (this.space.watchInterval && this.currentPage === meta.name) {
console.log("Page changed elsewhere, reloading");
this.flashNotification("Page changed elsewhere, reloading");
@@ -485,7 +472,6 @@ export class Editor {
// Kick off background sync
this.syncService.start();
this.collabManager.start();
this.eventHook.addLocalListener("sync:success", async (operations) => {
// console.log("Operations", operations);
@@ -738,7 +724,7 @@ export class Editor {
let touchCount = 0;
return EditorState.create({
doc: this.collabState ? this.collabState.ytext.toString() : text,
doc: text,
extensions: [
// Not using CM theming right now, but some extensions depend on the "dark" thing
EditorView.theme({}, { dark: this.viewState.uiOptions.darkMode }),
@@ -934,7 +920,6 @@ export class Editor {
...searchKeymap,
...historyKeymap,
...completionKeymap,
...(this.collabState ? yUndoManagerKeymap : []),
indentWithTab,
...commandKeyBindings,
{
@@ -1057,7 +1042,6 @@ export class Editor {
pasteLinkExtension,
attachmentExtension(this),
closeBrackets(),
...[this.collabState ? this.collabState.collabExtension() : []],
],
});
}
@@ -1070,7 +1054,7 @@ export class Editor {
await Promise.all((await this.space.listPlugs()).map(async (plugName) => {
try {
await this.system.load(
new URL(`/.fs/${plugName}`, location.href),
new URL(plugName, location.origin),
createSandbox,
);
} catch (e: any) {
@@ -1181,6 +1165,13 @@ export class Editor {
name = this.settings!.indexPage;
}
if (!isValidPageName(name)) {
return this.flashNotification(
"Invalid page name: page names cannot end with a file extension",
"error",
);
}
if (newWindow) {
const win = window.open(`${location.origin}/${name}`, "_blank");
if (win) {
@@ -1206,10 +1197,6 @@ export class Editor {
this.space.unwatchPage(previousPage);
if (previousPage !== pageName) {
await this.save(true);
// And stop the collab session
if (this.collabState) {
this.stopCollab();
}
}
}
@@ -1222,6 +1209,9 @@ export class Editor {
let doc;
try {
doc = await this.space.readPage(pageName);
if (doc.meta.contentType.startsWith("text/html")) {
throw new Error("Got HTML page, not markdown");
}
} catch (e: any) {
// Not found, new page
console.log("Creating new page", pageName);
@@ -1578,50 +1568,4 @@ export class Editor {
}
return;
}
startCollab(
serverUrl: string,
token: string,
username: string,
isLocalCollab = false,
) {
if (this.collabState) {
// Clean up old collab state
this.collabState.stop();
}
const initialText = this.editorView!.state.sliceDoc();
this.collabState = new CollabState(
serverUrl,
`${this.currentPage!}.md`,
token,
username,
this.syncService,
isLocalCollab,
);
this.collabState.collabProvider.on("synced", () => {
if (this.collabState!.ytext.toString() === "") {
console.log(
"[Collab]",
"Synced value is empty (new collab session), inserting local copy",
);
this.collabState!.ytext.insert(0, initialText);
}
});
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();
}
}
-2
View File
@@ -35,13 +35,11 @@
window.silverBulletConfig = {
// These {{VARIABLES}} are replaced by http_server.ts
spaceFolderPath: "{{SPACE_PATH}}",
syncEndpoint: "{{SYNC_ENDPOINT}}",
};
// But in case these variables aren't replaced by the server, fall back fully static mode (no sync)
if (window.silverBulletConfig.spaceFolderPath.includes("{{")) {
window.silverBulletConfig = {
spaceFolderPath: "",
syncEndpoint: "/.fs"
};
}
</script>
+78 -73
View File
@@ -24,42 +24,40 @@ const precacheFiles = Object.fromEntries([
self.addEventListener("install", (event: any) => {
console.log("[Service worker]", "Installing service worker...");
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log(
"[Service worker]",
"Now pre-caching client files",
);
return cache.addAll(Object.values(precacheFiles)).then(() => {
console.log(
"[Service worker]",
Object.keys(precacheFiles).length,
"client files cached",
);
// @ts-ignore: No need to wait
self.skipWaiting();
});
}),
(async () => {
const cache = await caches.open(CACHE_NAME);
console.log(
"[Service worker]",
"Now pre-caching client files",
);
await cache.addAll(Object.values(precacheFiles));
console.log(
"[Service worker]",
Object.keys(precacheFiles).length,
"client files cached",
);
// @ts-ignore: No need to wait
self.skipWaiting();
})(),
);
});
self.addEventListener("activate", (event: any) => {
console.log("[Service worker]", "Activating new service worker!!!");
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
(async () => {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log("[Service worker]", "Removing old cache", cacheName);
return caches.delete(cacheName);
}
}),
).then(() => {
// Let's activate ourselves for all existing clients
// @ts-ignore: No need to wait, clients is a serviceworker thing
return clients.claim();
});
}),
);
// @ts-ignore: No need to wait
return clients.claim();
})(),
);
});
@@ -75,6 +73,15 @@ self.addEventListener("fetch", (event: any) => {
event.respondWith(
(async () => {
const request = event.request;
// console.log("Getting request", request, [...request.headers.entries()]);
// Any request with the X-Sync-Mode header originates from the sync engine: pass it on to the server
if (request.headers.has("x-sync-mode")) {
return fetch(request);
}
// Try the static (client) file cache first
const cachedResponse = await caches.match(cacheKey);
// Return the cached response if found
@@ -82,67 +89,65 @@ self.addEventListener("fetch", (event: any) => {
return cachedResponse;
}
const requestUrl = new URL(event.request.url);
const requestUrl = new URL(request.url);
const pathname = requestUrl.pathname;
// console.log("In service worker, pathname is", pathname);
// Are we fetching a URL from the same origin as the app? If not, we don't handle it here
const fetchingLocal = location.host === requestUrl.host;
if (!fetchingLocal) {
return fetch(event.request);
// Are we fetching a URL from the same origin as the app? If not, we don't handle it and pass it on
if (location.host !== requestUrl.host) {
return fetch(request);
}
// If this is a /.fs request, this can either be a plug worker load or an attachment load
if (pathname.startsWith("/.fs")) {
if (!fileContentTable || event.request.headers.has("x-sync-mode")) {
// Not initialzed yet, or explicitly in sync mode (so direct server communication requested)
return fetch(event.request);
}
// console.log(
// "Attempting to serve file from locally synced space:",
// pathname,
// );
const path = decodeURIComponent(
requestUrl.pathname.slice("/.fs/".length),
);
const data = await fileContentTable.get(path);
if (data) {
// console.log("Serving from space", path);
if (!data.meta) {
// Legacy database not fully synced yet
data.meta = (await fileMetatable!.get(path))!;
}
return new Response(
data.data,
{
headers: {
"Content-type": data.meta.contentType,
"Content-Length": "" + data.meta.size,
"X-Permission": data.meta.perm,
"X-Last-Modified": "" + data.meta.lastModified,
},
},
);
} else {
console.error(
"Did not find file in locally synced space",
path,
);
return new Response("Not found", {
status: 404,
});
}
// If this is a /*.* request, this can either be a plug worker load or an attachment load
if (/\/.+\.[a-zA-Z]+$/.test(pathname)) {
return handleLocalFileRequest(request, pathname);
} else if (pathname === "/.auth") {
return fetch(event.request);
return fetch(request);
} else {
// Must be a page URL, let's serve index.html which will handle it
return (await caches.match(precacheFiles["/"])) || fetch(event.request);
return (await caches.match(precacheFiles["/"])) || fetch(request);
}
})(),
);
});
async function handleLocalFileRequest(
request: Request,
pathname: string,
): Promise<Response> {
if (!fileContentTable) {
// Not initialzed yet, or explicitly in sync mode (so direct server communication requested)
return fetch(request);
}
const path = decodeURIComponent(pathname.slice(1));
const data = await fileContentTable.get(path);
if (data) {
// console.log("Serving from space", path);
if (!data.meta) {
// Legacy database not fully synced yet
data.meta = (await fileMetatable!.get(path))!;
}
return new Response(
data.data,
{
headers: {
"Content-type": data.meta.contentType,
"Content-Length": "" + data.meta.size,
"X-Permission": data.meta.perm,
"X-Last-Modified": "" + data.meta.lastModified,
},
},
);
} else {
console.error(
"Did not find file in locally synced space",
path,
);
return new Response("Not found", {
status: 404,
});
}
}
self.addEventListener("message", (event: any) => {
if (event.data.type === "flushCache") {
caches.delete(CACHE_NAME)
+7 -129
View File
@@ -17,7 +17,7 @@ const syncStartTimeKey = "syncStartTime";
// Keeps the last time an activity was registered, used to detect if a sync is still alive and whether a new one should be started already
const syncLastActivityKey = "syncLastActivity";
const syncExcludePrefix = "syncExclude:";
const syncInitialFullSyncCompletedKey = "syncInitialFullSyncCompleted";
// maximum time between two activities before we consider a sync crashed
const syncMaxIdleTimeout = 1000 * 20; // 20s
@@ -56,17 +56,9 @@ export class SyncService {
await this.syncFile(`${name}.md`);
});
eventHook.addLocalListener("editor:pageSaved", async (name, meta) => {
eventHook.addLocalListener("editor:pageSaved", async (name) => {
const path = `${name}.md`;
await this.syncFile(path);
if (await this.isExcludedFromSync(path)) {
// So we're editing a page and just saved it, but it's excluded from sync
// Assumption: we're in collab mode for this file, so we're going to constantly update our local hash
// console.log(
// "Locally updating last modified in snapshot because we're in collab mode",
// );
await this.updateLocalLastModified(path, meta.lastModified);
}
});
}
@@ -86,10 +78,9 @@ export class SyncService {
return true;
}
async hasInitialSyncCompleted(): Promise<boolean> {
hasInitialSyncCompleted(): Promise<boolean> {
// Initial sync has happened when sync progress has been reported at least once, but the syncStartTime has been reset (which happens after sync finishes)
return !(await this.kvStore.has(syncStartTimeKey)) &&
(await this.kvStore.has(syncLastActivityKey));
return this.kvStore.has(syncInitialFullSyncCompletedKey);
}
async registerSyncStart(): Promise<void> {
@@ -116,40 +107,7 @@ export class SyncService {
async registerSyncStop(): Promise<void> {
await this.registerSyncProgress();
await this.kvStore.del(syncStartTimeKey);
}
// Temporarily exclude a specific file from sync (e.g. when in collab mode)
excludeFromSync(path: string): Promise<void> {
return this.kvStore.set(syncExcludePrefix + path, Date.now());
}
unExcludeFromSync(path: string): Promise<void> {
return this.kvStore.del(syncExcludePrefix + path);
}
async isExcludedFromSync(path: string): Promise<boolean> {
const lastExcluded = await this.kvStore.get(syncExcludePrefix + path);
return lastExcluded && Date.now() - lastExcluded < syncMaxIdleTimeout;
}
async fetchAllExcludedFromSync(): Promise<string[]> {
const entries = await this.kvStore.queryPrefix(syncExcludePrefix);
const expiredPaths: string[] = [];
const now = Date.now();
const result = entries.filter(({ key, value }) => {
if (now - value > syncMaxIdleTimeout) {
expiredPaths.push(key);
return false;
}
return true;
}).map(({ key }) => key.slice(syncExcludePrefix.length));
if (expiredPaths.length > 0) {
console.log("Purging expired sync exclusions: ", expiredPaths);
await this.kvStore.batchDelete(expiredPaths);
}
return result;
await this.kvStore.set(syncInitialFullSyncCompletedKey, true);
}
async getSnapshot(): Promise<Map<string, SyncStatusItem>> {
@@ -167,83 +125,6 @@ export class SyncService {
}
}
// When in collab mode, we delegate the sync to the CDRT engine, to avoid conflicts, we try to keep the lastModified time in sync with the remote
async updateRemoteLastModified(path: string, lastModified: number) {
await this.noOngoingSync();
await this.registerSyncStart();
const snapshot = await this.getSnapshot();
const entry = snapshot.get(path);
if (entry) {
snapshot.set(path, [entry[0], lastModified]);
} else {
// In the unlikely scenario that a space first openen on a collab page before every being synced
try {
console.log(
"Received lastModified time for file not in snapshot",
path,
lastModified,
);
snapshot.set(path, [
(await this.localSpacePrimitives.getFileMeta(path)).lastModified,
lastModified,
]);
} catch (e) {
console.warn(
"Received lastModified time for non-existing file not in snapshot",
path,
lastModified,
e,
);
}
}
await this.saveSnapshot(snapshot);
await this.registerSyncStop();
}
// Reach out out to remote space, fetch the latest lastModified time and update the local snapshot
// This is used when exiting collab mode
async fetchAndPersistRemoteLastModified(path: string) {
const meta = await this.remoteSpace.getFileMeta(path);
await this.updateRemoteLastModified(
path,
meta.lastModified,
);
}
// When in collab mode, we delegate the sync to the CDRT engine, to avoid conflicts, we try to keep the lastModified time in sync when local changes happen
async updateLocalLastModified(path: string, lastModified: number) {
await this.noOngoingSync();
await this.registerSyncStart();
const snapshot = await this.getSnapshot();
const entry = snapshot.get(path);
if (entry) {
snapshot.set(path, [lastModified, entry[1]]);
} else {
// In the unlikely scenario that a space first openen on a collab page before every being synced
try {
console.log(
"Setting lastModified time for file not in snapshot",
path,
lastModified,
);
snapshot.set(path, [
lastModified,
(await this.localSpacePrimitives.getFileMeta(path)).lastModified,
]);
} catch (e) {
console.warn(
"Received lastModified time for non-existing file not in snapshot",
path,
lastModified,
e,
);
}
}
await this.saveSnapshot(snapshot);
await this.registerSyncStop();
// console.log("All done!");
}
start() {
this.syncSpace().catch(
console.error,
@@ -271,14 +152,11 @@ export class SyncService {
await this.registerSyncStart();
let operations = 0;
const snapshot = await this.getSnapshot();
// Fetch the list of files that are excluded from sync (e.g. because they're in collab mode)
const excludedFromSync = await this.fetchAllExcludedFromSync();
// console.log("Excluded from sync", excludedFromSync);
try {
operations = await this.spaceSync!.syncFiles(
snapshot,
(path) =>
this.isSyncCandidate(path) && !excludedFromSync.includes(path),
(path) => this.isSyncCandidate(path),
);
this.eventHook.dispatchEvent("sync:success", operations);
} catch (e: any) {
@@ -295,7 +173,7 @@ export class SyncService {
// console.log("Already syncing");
return;
}
if (!this.isSyncCandidate(name) || (await this.isExcludedFromSync(name))) {
if (!this.isSyncCandidate(name)) {
return;
}
await this.registerSyncStart();
-20
View File
@@ -1,20 +0,0 @@
import { SysCallMapping } from "../../plugos/system.ts";
import type { Editor } from "../editor.tsx";
export function collabSyscalls(editor: Editor): SysCallMapping {
return {
"collab.start": (
_ctx,
serverUrl: string,
token: string,
username: string,
) => {
editor.startCollab(serverUrl, token, username);
},
"collab.stop": (
_ctx,
) => {
editor.stopCollab();
},
};
}
+1 -1
View File
@@ -21,7 +21,7 @@ export function sandboxFetchSyscalls(
return performLocalFetch(url, options);
}
const resp = httpSpacePrimitives.authenticatedFetch(
httpSpacePrimitives.url,
`${httpSpacePrimitives.url}/.rpc`,
{
method: "POST",
body: JSON.stringify({
+1 -1
View File
@@ -14,7 +14,7 @@ export function shellSyscalls(
throw new Error("Not supported in fully local mode");
}
const resp = httpSpacePrimitives.authenticatedFetch(
httpSpacePrimitives.url,
`${httpSpacePrimitives.url}/.rpc`,
{
method: "POST",
body: JSON.stringify({