1
0

Sync current open page every 5s

This commit is contained in:
Zef Hemel 2023-08-07 20:42:52 +02:00
parent 4af7afa4aa
commit 7498cc1ecb
5 changed files with 60 additions and 20 deletions

View File

@ -3,6 +3,7 @@ import buildMarkdown from "../markdown_parser/parser.ts";
import { parse } from "../markdown_parser/parse_tree.ts";
import type { FileMeta } from "../types.ts";
import { SpacePrimitives } from "./space_primitives.ts";
import { EventEmitter } from "../../plugos/event.ts";
type SyncHash = number;
@ -28,13 +29,19 @@ export type SyncOptions = {
onSyncProgress?: (syncStatus: SyncStatus) => void;
};
type SyncDirection = "primary->secondary" | "secondary->primary";
export type SyncEvents = {
fileSynced: (meta: FileMeta, direction: SyncDirection) => void;
};
// Implementation of this algorithm https://unterwaditzer.net/2016/sync-algorithm.html
export class SpaceSync {
export class SpaceSync extends EventEmitter<SyncEvents> {
constructor(
private primary: SpacePrimitives,
private secondary: SpacePrimitives,
readonly options: SyncOptions,
) {
super();
}
async syncFiles(
@ -143,6 +150,7 @@ export class SpaceSync {
writtenMeta.lastModified,
]);
operations++;
await this.emit("fileSynced", writtenMeta, "primary->secondary");
} else if (
secondaryHash !== undefined && primaryHash === undefined &&
!snapshot.has(name)
@ -165,6 +173,7 @@ export class SpaceSync {
secondaryHash,
]);
operations++;
await this.emit("fileSynced", writtenMeta, "secondary->primary");
} else if (
primaryHash !== undefined && snapshot.has(name) &&
secondaryHash === undefined
@ -227,6 +236,7 @@ export class SpaceSync {
writtenMeta.lastModified,
]);
operations++;
await this.emit("fileSynced", writtenMeta, "primary->secondary");
} else if (
primaryHash !== undefined && secondaryHash !== undefined &&
snapshot.get(name) &&
@ -251,6 +261,7 @@ export class SpaceSync {
secondaryHash,
]);
operations++;
await this.emit("fileSynced", writtenMeta, "secondary->primary");
} else if (
( // File changed on both ends, but we don't have any info in the snapshot (resync scenario?): have to run through conflict handling
primaryHash !== undefined && secondaryHash !== undefined &&

View File

@ -21,7 +21,7 @@ import { PlugSpacePrimitives } from "../common/spaces/plug_space_primitives.ts";
import { IndexedDBSpacePrimitives } from "../common/spaces/indexeddb_space_primitives.ts";
import { FileMetaSpacePrimitives } from "../common/spaces/file_meta_space_primitives.ts";
import { EventedSpacePrimitives } from "../common/spaces/evented_space_primitives.ts";
import { SyncService } from "./sync_service.ts";
import { pageSyncInterval, SyncService } from "./sync_service.ts";
import { simpleHash } from "../common/crypto.ts";
import { DexieKVStore } from "../plugos/lib/kv_store.dexie.ts";
import { SyncStatus } from "../common/spaces/sync.ts";
@ -166,6 +166,10 @@ export class Client {
this.loadCustomStyles().catch(console.error);
await this.dispatchAppEvent("editor:init");
setInterval(() => {
this.syncService.syncFile(`${this.currentPage!}.md`);
}, pageSyncInterval);
}
private initSync() {
@ -215,6 +219,14 @@ export class Client {
Math.round(status.filesProcessed / status.totalFiles * 100),
);
});
this.syncService.spaceSync.on({
fileSynced: (meta, direction) => {
if (meta.name.endsWith(".md") && direction === "secondary->primary") {
// We likely polled the currently open page which trigggered a local update, let's update the editor accordingly
this.space.getPageMeta(meta.name.slice(0, -3));
}
},
});
}
private initNavigator() {

View File

@ -67,7 +67,7 @@ export function MiniEditor(
useEffect(() => {
if (editorDiv.current) {
console.log("Creating editor view");
// console.log("Creating editor view");
const editorView = new EditorView({
state: buildEditorState(),
parent: editorDiv.current!,

View File

@ -14,6 +14,9 @@ const syncSnapshotKey = "syncSnapshot";
// Keeps the start time of an ongoing sync, is reset once the sync is done
const syncStartTimeKey = "syncStartTime";
// Keeps the start time of the last full sync cycle
const syncLastFullCycleKey = "syncLastFullCycle";
// 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";
@ -23,7 +26,10 @@ const syncInitialFullSyncCompletedKey = "syncInitialFullSyncCompleted";
const syncMaxIdleTimeout = 1000 * 20; // 20s
// How often to sync the whole space
const syncInterval = 10 * 1000; // Every 10s
const spaceSyncInterval = 10 * 1000; // Every 10s, but because of the check this may happen mid-cycle so after ~15s
// Used from Client
export const pageSyncInterval = 5000;
/**
* The SyncService primarily wraps the SpaceSync engine but also coordinates sync between
@ -88,15 +94,24 @@ export class SyncService {
return this.kvStore.has(syncInitialFullSyncCompletedKey);
}
async registerSyncStart(): Promise<void> {
async registerSyncStart(fullSync: boolean): Promise<void> {
// Assumption: this is called after an isSyncing() check
await this.kvStore.batchSet([{
key: syncStartTimeKey,
value: Date.now(),
}, {
key: syncLastActivityKey,
value: Date.now(),
}]);
await this.kvStore.batchSet([
{
key: syncStartTimeKey,
value: Date.now(),
},
{
key: syncLastActivityKey,
value: Date.now(),
},
...fullSync // If this is a full sync cycle
? [{
key: syncLastFullCycleKey,
value: Date.now(),
}]
: [],
]);
}
async registerSyncProgress(status?: SyncStatus): Promise<void> {
@ -150,16 +165,18 @@ export class SyncService {
setInterval(async () => {
try {
const lastActivity = (await this.kvStore.get(syncLastActivityKey)) || 0;
if (lastActivity && Date.now() - lastActivity > syncInterval) {
// It's been a while since the last activity, let's sync the whole space
// The reason to do this check is that there may be multiple tabs open each with their sync cycle
await this.syncSpace();
if (!await this.isSyncing()) {
const lastFullCycle =
(await this.kvStore.get(syncLastFullCycleKey)) || 0;
if (lastFullCycle && Date.now() - lastFullCycle > spaceSyncInterval) {
// It's been a while since the last full cycle, let's sync the whole space
await this.syncSpace();
}
}
} catch (e: any) {
console.error(e);
}
}, syncInterval / 2); // check every half the sync cycle because actually running the sync takes some time therefore we don't want to wait for the full cycle
}, spaceSyncInterval / 2); // check every half the sync cycle because actually running the sync takes some time therefore we don't want to wait for the full cycle
}
async syncSpace(): Promise<number> {
@ -167,7 +184,7 @@ export class SyncService {
console.log("Already syncing");
return 0;
}
await this.registerSyncStart();
await this.registerSyncStart(true);
let operations = 0;
const snapshot = await this.getSnapshot();
// console.log("Excluded from sync", excludedFromSync);
@ -197,7 +214,7 @@ export class SyncService {
console.log("Already syncing, aborting individual file sync for", name);
return;
}
await this.registerSyncStart();
await this.registerSyncStart(false);
console.log("Syncing file", name);
const snapshot = await this.getSnapshot();
try {

View File