Enormous refactor adding IndexedDB space and syncing.
parent
eb781b9e19
commit
fff2690e99
|
@ -93,17 +93,21 @@ export async function updateMaterializedQueriesOnPage(pageName: string) {
|
||||||
return `${startQuery}\n${results.sort().join("\n")}\n${endQuery}`;
|
return `${startQuery}\n${results.sort().join("\n")}\n${endQuery}`;
|
||||||
case "link":
|
case "link":
|
||||||
let uniqueLinks = new Set<string>();
|
let uniqueLinks = new Set<string>();
|
||||||
|
console.log("Here!!");
|
||||||
for (let { key, page, value: name } of await scanPrefixGlobal(
|
for (let { key, page, value: name } of await scanPrefixGlobal(
|
||||||
`pl:${pageName}:`
|
`pl:${pageName}:`
|
||||||
)) {
|
)) {
|
||||||
|
console.log("Here!!");
|
||||||
let [, pos] = key.split(":");
|
let [, pos] = key.split(":");
|
||||||
if (!filter || (filter && name.includes(filter))) {
|
if (!filter || (filter && name.includes(filter))) {
|
||||||
uniqueLinks.add(name);
|
uniqueLinks.add(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log("Here!!");
|
||||||
for (const uniqueResult of uniqueLinks) {
|
for (const uniqueResult of uniqueLinks) {
|
||||||
results.push(`* [[${uniqueResult}]]`);
|
results.push(`* [[${uniqueResult}]]`);
|
||||||
}
|
}
|
||||||
|
console.log("Here!!");
|
||||||
return `${startQuery}\n${results.sort().join("\n")}\n${endQuery}`;
|
return `${startQuery}\n${results.sort().join("\n")}\n${endQuery}`;
|
||||||
case "item":
|
case "item":
|
||||||
for (let {
|
for (let {
|
||||||
|
|
|
@ -68,6 +68,7 @@ export class ExpressServer {
|
||||||
|
|
||||||
// Page list
|
// Page list
|
||||||
fsRouter.route("/").get(async (req, res) => {
|
fsRouter.route("/").get(async (req, res) => {
|
||||||
|
res.header("Now-Timestamp", "" + Date.now());
|
||||||
res.json(await this.storage.listPages());
|
res.json(await this.storage.listPages());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,7 @@ export class DiskStorage implements Storage {
|
||||||
if (lastModified) {
|
if (lastModified) {
|
||||||
let d = new Date(lastModified);
|
let d = new Date(lastModified);
|
||||||
console.log("Going to set the modified time", d);
|
console.log("Going to set the modified time", d);
|
||||||
await utimes(localPath, lastModified, lastModified);
|
await utimes(localPath, d, d);
|
||||||
}
|
}
|
||||||
// Fetch new metadata
|
// Fetch new metadata
|
||||||
const s = await stat(localPath);
|
const s = await stat(localPath);
|
||||||
|
|
|
@ -2,23 +2,33 @@ import { Editor } from "./editor";
|
||||||
import { safeRun } from "./util";
|
import { safeRun } from "./util";
|
||||||
import { WatchableSpace } from "./spaces/cache_space";
|
import { WatchableSpace } from "./spaces/cache_space";
|
||||||
import { HttpRestSpace } from "./spaces/httprest_space";
|
import { HttpRestSpace } from "./spaces/httprest_space";
|
||||||
|
import { IndexedDBSpace } from "./spaces/indexeddb_space";
|
||||||
|
import { SpaceSync } from "./spaces/sync";
|
||||||
|
|
||||||
// let localSpace = new WatchableSpace(new IndexedDBSpace("pages"), true);
|
let localSpace = new WatchableSpace(new IndexedDBSpace("pages"), true);
|
||||||
// localSpace.watch();
|
localSpace.watch();
|
||||||
let serverSpace = new WatchableSpace(new HttpRestSpace(""), true);
|
let serverSpace = new WatchableSpace(new HttpRestSpace(""), true);
|
||||||
serverSpace.watch();
|
// serverSpace.watch();
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
// window.syncer = async () => {
|
window.syncer = async () => {
|
||||||
// let lastSync = +(localStorage.getItem("lastSync") || "0");
|
let lastLocalSync = +(localStorage.getItem("lastLocalSync") || "0"),
|
||||||
// let syncer = new SpaceSync(serverSpace, localSpace, lastSync, "_trash/");
|
lastRemoteSync = +(localStorage.getItem("lastRemoteSync") || "0");
|
||||||
// await syncer.syncPages(
|
let syncer = new SpaceSync(
|
||||||
// SpaceSync.primaryConflictResolver(serverSpace, localSpace)
|
serverSpace,
|
||||||
// );
|
localSpace,
|
||||||
// localStorage.setItem("lastSync", "" + syncer.lastSync);
|
lastRemoteSync,
|
||||||
// console.log("Done!");
|
lastLocalSync,
|
||||||
// };
|
"_trash/"
|
||||||
let editor = new Editor(serverSpace, document.getElementById("root")!);
|
);
|
||||||
|
await syncer.syncPages(
|
||||||
|
SpaceSync.primaryConflictResolver(serverSpace, localSpace)
|
||||||
|
);
|
||||||
|
localStorage.setItem("lastLocalSync", "" + syncer.secondaryLastSync);
|
||||||
|
localStorage.setItem("lastRemoteSync", "" + syncer.primaryLastSync);
|
||||||
|
console.log("Done!");
|
||||||
|
};
|
||||||
|
let editor = new Editor(localSpace, document.getElementById("root")!);
|
||||||
|
|
||||||
safeRun(async () => {
|
safeRun(async () => {
|
||||||
await editor.init();
|
await editor.init();
|
||||||
|
|
|
@ -33,6 +33,15 @@ export function TopBar({
|
||||||
<FontAwesomeIcon icon={faFileLines} />
|
<FontAwesomeIcon icon={faFileLines} />
|
||||||
</span>
|
</span>
|
||||||
<span className="current-page">{prettyName(pageName)}</span>
|
<span className="current-page">{prettyName(pageName)}</span>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
// @ts-ignore
|
||||||
|
window.syncer();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sync
|
||||||
|
</button>
|
||||||
{notifications.length > 0 && (
|
{notifications.length > 0 && (
|
||||||
<div className="status">
|
<div className="status">
|
||||||
{notifications.map((notification) => (
|
{notifications.map((notification) => (
|
||||||
|
|
|
@ -32,7 +32,7 @@ import { smartQuoteKeymap } from "./smart_quotes";
|
||||||
import { WatchableSpace } from "./spaces/cache_space";
|
import { WatchableSpace } from "./spaces/cache_space";
|
||||||
import customMarkdownStyle from "./style";
|
import customMarkdownStyle from "./style";
|
||||||
import { editorSyscalls } from "./syscalls/editor";
|
import { editorSyscalls } from "./syscalls/editor";
|
||||||
import { indexerSyscalls } from "./syscalls/indexer";
|
import { indexerSyscalls } from "./syscalls";
|
||||||
import { spaceSyscalls } from "./syscalls/space";
|
import { spaceSyscalls } from "./syscalls/space";
|
||||||
import { Action, AppViewState, initialViewState } from "./types";
|
import { Action, AppViewState, initialViewState } from "./types";
|
||||||
import { SilverBulletHooks } from "../common/manifest";
|
import { SilverBulletHooks } from "../common/manifest";
|
||||||
|
|
|
@ -8,7 +8,7 @@ const pageWatchInterval = 2000;
|
||||||
const trashPrefix = "_trash/";
|
const trashPrefix = "_trash/";
|
||||||
const plugPrefix = "_plug/";
|
const plugPrefix = "_plug/";
|
||||||
|
|
||||||
export class WatchableSpace extends EventEmitter<SpaceEvents> implements Space {
|
export class WatchableSpace extends EventEmitter<SpaceEvents> {
|
||||||
pageMetaCache = new Map<string, PageMeta>();
|
pageMetaCache = new Map<string, PageMeta>();
|
||||||
watchedPages = new Set<string>();
|
watchedPages = new Set<string>();
|
||||||
private initialPageListLoad = true;
|
private initialPageListLoad = true;
|
||||||
|
@ -46,7 +46,7 @@ export class WatchableSpace extends EventEmitter<SpaceEvents> implements Space {
|
||||||
safeRun(async () => {
|
safeRun(async () => {
|
||||||
let newPageList = await this.space.fetchPageList();
|
let newPageList = await this.space.fetchPageList();
|
||||||
let deletedPages = new Set<string>(this.pageMetaCache.keys());
|
let deletedPages = new Set<string>(this.pageMetaCache.keys());
|
||||||
newPageList.forEach((meta) => {
|
newPageList.pages.forEach((meta) => {
|
||||||
const pageName = meta.name;
|
const pageName = meta.name;
|
||||||
const oldPageMeta = this.pageMetaCache.get(pageName);
|
const oldPageMeta = this.pageMetaCache.get(pageName);
|
||||||
const newPageMeta = {
|
const newPageMeta = {
|
||||||
|
@ -112,11 +112,11 @@ export class WatchableSpace extends EventEmitter<SpaceEvents> implements Space {
|
||||||
await this.writePage(
|
await this.writePage(
|
||||||
`${trashPrefix}${name}`,
|
`${trashPrefix}${name}`,
|
||||||
pageData.text,
|
pageData.text,
|
||||||
true,
|
false,
|
||||||
deleteDate
|
deleteDate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await this.space.deletePage(name, deleteDate);
|
await this.space.deletePage(name);
|
||||||
|
|
||||||
this.pageMetaCache.delete(name);
|
this.pageMetaCache.delete(name);
|
||||||
this.emit("pageDeleted", name);
|
this.emit("pageDeleted", name);
|
||||||
|
@ -210,7 +210,7 @@ export class WatchableSpace extends EventEmitter<SpaceEvents> implements Space {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchPageList(): Promise<Set<PageMeta>> {
|
fetchPageList(): Promise<{ pages: Set<PageMeta>; nowTimestamp: number }> {
|
||||||
return this.space.fetchPageList();
|
return this.space.fetchPageList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,10 @@ export class HttpRestSpace implements Space {
|
||||||
this.plugUrl = url + "/plug";
|
this.plugUrl = url + "/plug";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchPageList(): Promise<Set<PageMeta>> {
|
public async fetchPageList(): Promise<{
|
||||||
|
pages: Set<PageMeta>;
|
||||||
|
nowTimestamp: number;
|
||||||
|
}> {
|
||||||
let req = await fetch(this.pageUrl, {
|
let req = await fetch(this.pageUrl, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
|
@ -25,7 +28,10 @@ export class HttpRestSpace implements Space {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return {
|
||||||
|
pages: result,
|
||||||
|
nowTimestamp: +req.headers.get("Now-Timestamp")!,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
||||||
|
|
|
@ -12,7 +12,7 @@ type Page = {
|
||||||
export class IndexedDBSpace implements Space {
|
export class IndexedDBSpace implements Space {
|
||||||
private pageTable: Table<Page, string>;
|
private pageTable: Table<Page, string>;
|
||||||
|
|
||||||
constructor(dbName: string) {
|
constructor(dbName: string, readonly timeSkew: number = 0) {
|
||||||
const db = new Dexie(dbName);
|
const db = new Dexie(dbName);
|
||||||
db.version(1).stores({
|
db.version(1).stores({
|
||||||
page: "name",
|
page: "name",
|
||||||
|
@ -42,13 +42,19 @@ export class IndexedDBSpace implements Space {
|
||||||
return plug.invoke(name, args);
|
return plug.invoke(name, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchPageList(): Promise<Set<PageMeta>> {
|
async fetchPageList(): Promise<{
|
||||||
|
pages: Set<PageMeta>;
|
||||||
|
nowTimestamp: number;
|
||||||
|
}> {
|
||||||
let allPages = await this.pageTable.toArray();
|
let allPages = await this.pageTable.toArray();
|
||||||
let set = new Set(allPages.map((p) => p.meta));
|
return {
|
||||||
return set;
|
pages: new Set(allPages.map((p) => p.meta)),
|
||||||
|
nowTimestamp: Date.now() + this.timeSkew,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any> {
|
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any> {
|
||||||
|
console.log("Going this", name);
|
||||||
return plug.syscall(name, args);
|
return plug.syscall(name, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +75,7 @@ export class IndexedDBSpace implements Space {
|
||||||
): Promise<PageMeta> {
|
): Promise<PageMeta> {
|
||||||
let meta = {
|
let meta = {
|
||||||
name,
|
name,
|
||||||
lastModified: lastModified ? lastModified : new Date().getTime(),
|
lastModified: lastModified ? lastModified : Date.now() + this.timeSkew,
|
||||||
};
|
};
|
||||||
await this.pageTable.put({
|
await this.pageTable.put({
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -13,7 +13,7 @@ export type SpaceEvents = {
|
||||||
|
|
||||||
export interface Space {
|
export interface Space {
|
||||||
// Pages
|
// Pages
|
||||||
fetchPageList(): Promise<Set<PageMeta>>;
|
fetchPageList(): Promise<{ pages: Set<PageMeta>; nowTimestamp: number }>;
|
||||||
readPage(name: string): Promise<{ text: string; meta: PageMeta }>;
|
readPage(name: string): Promise<{ text: string; meta: PageMeta }>;
|
||||||
getPageMeta(name: string): Promise<PageMeta>;
|
getPageMeta(name: string): Promise<PageMeta>;
|
||||||
writePage(
|
writePage(
|
||||||
|
@ -22,7 +22,7 @@ export interface Space {
|
||||||
selfUpdate?: boolean,
|
selfUpdate?: boolean,
|
||||||
lastModified?: number
|
lastModified?: number
|
||||||
): Promise<PageMeta>;
|
): Promise<PageMeta>;
|
||||||
deletePage(name: string, deleteDate?: number): Promise<void>;
|
deletePage(name: string): Promise<void>;
|
||||||
|
|
||||||
// Plugs
|
// Plugs
|
||||||
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any>;
|
proxySyscall(plug: Plug<any>, name: string, args: any[]): Promise<any>;
|
||||||
|
|
|
@ -9,32 +9,33 @@ require("fake-indexeddb/auto");
|
||||||
|
|
||||||
test("Test store", async () => {
|
test("Test store", async () => {
|
||||||
let primary = new WatchableSpace(new IndexedDBSpace("primary"), true);
|
let primary = new WatchableSpace(new IndexedDBSpace("primary"), true);
|
||||||
let secondary = new WatchableSpace(new IndexedDBSpace("secondary"), true);
|
let secondary = new WatchableSpace(
|
||||||
let sync = new SpaceSync(primary, secondary, 0, "_trash/");
|
new IndexedDBSpace("secondary", -5000),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
let sync = new SpaceSync(primary, secondary, 0, 0, "_trash/");
|
||||||
|
|
||||||
async function conflictResolver(pageMeta1: PageMeta, pageMeta2: PageMeta) {}
|
async function conflictResolver(pageMeta1: PageMeta, pageMeta2: PageMeta) {}
|
||||||
|
|
||||||
// Write one page to primary
|
// Write one page to primary
|
||||||
await primary.writePage("start", "Hello");
|
await primary.writePage("start", "Hello");
|
||||||
expect((await secondary.listPages()).size).toBe(0);
|
expect((await secondary.listPages()).size).toBe(0);
|
||||||
await sync.syncPages(conflictResolver);
|
await syncPages(conflictResolver);
|
||||||
expect((await secondary.listPages()).size).toBe(1);
|
expect((await secondary.listPages()).size).toBe(1);
|
||||||
expect((await secondary.readPage("start")).text).toBe("Hello");
|
expect((await secondary.readPage("start")).text).toBe("Hello");
|
||||||
let lastSync = sync.lastSync;
|
|
||||||
|
|
||||||
// Should be a no-op
|
// Should be a no-op
|
||||||
await sync.syncPages();
|
expect(await syncPages()).toBe(0);
|
||||||
expect(sync.lastSync).toBe(lastSync);
|
|
||||||
|
|
||||||
// Now let's make a change on the secondary
|
// Now let's make a change on the secondary
|
||||||
await secondary.writePage("start", "Hello!!");
|
await secondary.writePage("start", "Hello!!");
|
||||||
await secondary.writePage("test", "Test page");
|
await secondary.writePage("test", "Test page");
|
||||||
|
|
||||||
// And sync it
|
// And sync it
|
||||||
await sync.syncPages();
|
await syncPages();
|
||||||
|
|
||||||
expect((await primary.listPages()).size).toBe(2);
|
expect(primary.listPages().size).toBe(2);
|
||||||
expect((await secondary.listPages()).size).toBe(2);
|
expect(secondary.listPages().size).toBe(2);
|
||||||
|
|
||||||
expect((await primary.readPage("start")).text).toBe("Hello!!");
|
expect((await primary.readPage("start")).text).toBe("Hello!!");
|
||||||
|
|
||||||
|
@ -43,12 +44,12 @@ test("Test store", async () => {
|
||||||
await primary.writePage("start2", "2");
|
await primary.writePage("start2", "2");
|
||||||
await secondary.writePage("start3", "3");
|
await secondary.writePage("start3", "3");
|
||||||
await secondary.writePage("start4", "4");
|
await secondary.writePage("start4", "4");
|
||||||
await sync.syncPages();
|
await syncPages();
|
||||||
|
|
||||||
expect((await primary.listPages()).size).toBe(5);
|
expect((await primary.listPages()).size).toBe(5);
|
||||||
expect((await secondary.listPages()).size).toBe(5);
|
expect((await secondary.listPages()).size).toBe(5);
|
||||||
|
|
||||||
expect(await sync.syncPages()).toBe(0);
|
expect(await syncPages()).toBe(0);
|
||||||
|
|
||||||
console.log("Deleting pages");
|
console.log("Deleting pages");
|
||||||
// Delete some pages
|
// Delete some pages
|
||||||
|
@ -58,29 +59,29 @@ test("Test store", async () => {
|
||||||
console.log("Pages", await primary.listPages());
|
console.log("Pages", await primary.listPages());
|
||||||
console.log("Trash", await primary.listTrash());
|
console.log("Trash", await primary.listTrash());
|
||||||
|
|
||||||
await sync.syncPages();
|
await syncPages();
|
||||||
|
|
||||||
expect((await primary.listPages()).size).toBe(3);
|
expect((await primary.listPages()).size).toBe(3);
|
||||||
expect((await secondary.listPages()).size).toBe(3);
|
expect((await secondary.listPages()).size).toBe(3);
|
||||||
|
|
||||||
// No-op
|
// No-op
|
||||||
expect(await sync.syncPages()).toBe(0);
|
expect(await syncPages()).toBe(0);
|
||||||
|
|
||||||
await secondary.deletePage("start4");
|
await secondary.deletePage("start4");
|
||||||
await primary.deletePage("start2");
|
await primary.deletePage("start2");
|
||||||
|
|
||||||
await sync.syncPages();
|
await syncPages();
|
||||||
|
|
||||||
// Just "test" left
|
// Just "test" left
|
||||||
expect((await primary.listPages()).size).toBe(1);
|
expect((await primary.listPages()).size).toBe(1);
|
||||||
expect((await secondary.listPages()).size).toBe(1);
|
expect((await secondary.listPages()).size).toBe(1);
|
||||||
|
|
||||||
// No-op
|
// No-op
|
||||||
expect(await sync.syncPages()).toBe(0);
|
expect(await syncPages()).toBe(0);
|
||||||
|
|
||||||
await secondary.writePage("start", "I'm back");
|
await secondary.writePage("start", "I'm back");
|
||||||
|
|
||||||
await sync.syncPages();
|
await syncPages();
|
||||||
|
|
||||||
expect((await primary.readPage("start")).text).toBe("I'm back");
|
expect((await primary.readPage("start")).text).toBe("I'm back");
|
||||||
|
|
||||||
|
@ -88,10 +89,10 @@ test("Test store", async () => {
|
||||||
await primary.writePage("start", "Hello 1");
|
await primary.writePage("start", "Hello 1");
|
||||||
await secondary.writePage("start", "Hello 2");
|
await secondary.writePage("start", "Hello 2");
|
||||||
|
|
||||||
await sync.syncPages(SpaceSync.primaryConflictResolver(primary, secondary));
|
await syncPages(SpaceSync.primaryConflictResolver(primary, secondary));
|
||||||
|
|
||||||
// Sync conflicting copy back
|
// Sync conflicting copy back
|
||||||
await sync.syncPages();
|
await syncPages();
|
||||||
|
|
||||||
// Verify that primary won
|
// Verify that primary won
|
||||||
expect((await primary.readPage("start")).text).toBe("Hello 1");
|
expect((await primary.readPage("start")).text).toBe("Hello 1");
|
||||||
|
@ -100,4 +101,23 @@ test("Test store", async () => {
|
||||||
// test + start + start.conflicting copy
|
// test + start + start.conflicting copy
|
||||||
expect((await primary.listPages()).size).toBe(3);
|
expect((await primary.listPages()).size).toBe(3);
|
||||||
expect((await secondary.listPages()).size).toBe(3);
|
expect((await secondary.listPages()).size).toBe(3);
|
||||||
|
|
||||||
|
async function syncPages(
|
||||||
|
conflictResolver?: (
|
||||||
|
pageMeta1: PageMeta,
|
||||||
|
pageMeta2: PageMeta
|
||||||
|
) => Promise<void>
|
||||||
|
): Promise<number> {
|
||||||
|
// Awesome practice: adding sleeps to fix issues!
|
||||||
|
await sleep(2);
|
||||||
|
let n = await sync.syncPages(conflictResolver);
|
||||||
|
await sleep(2);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function sleep(ms: number = 5): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ export class SpaceSync {
|
||||||
constructor(
|
constructor(
|
||||||
private primary: WatchableSpace,
|
private primary: WatchableSpace,
|
||||||
private secondary: WatchableSpace,
|
private secondary: WatchableSpace,
|
||||||
public lastSync: number,
|
public primaryLastSync: number,
|
||||||
|
public secondaryLastSync: number,
|
||||||
private trashPrefix: string
|
private trashPrefix: string
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -33,14 +34,20 @@ export class SpaceSync {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncablePages(space: Space): Promise<PageMeta[]> {
|
async syncablePages(
|
||||||
return [...(await space.fetchPageList())].filter(
|
space: WatchableSpace
|
||||||
(pageMeta) => !pageMeta.name.startsWith(this.trashPrefix)
|
): Promise<{ pages: PageMeta[]; nowTimestamp: number }> {
|
||||||
);
|
let fetchResult = await space.fetchPageList();
|
||||||
|
return {
|
||||||
|
pages: [...fetchResult.pages].filter(
|
||||||
|
(pageMeta) => !pageMeta.name.startsWith(this.trashPrefix)
|
||||||
|
),
|
||||||
|
nowTimestamp: fetchResult.nowTimestamp,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async trashPages(space: Space): Promise<PageMeta[]> {
|
async trashPages(space: Space): Promise<PageMeta[]> {
|
||||||
return [...(await space.fetchPageList())]
|
return [...(await space.fetchPageList()).pages]
|
||||||
.filter((pageMeta) => pageMeta.name.startsWith(this.trashPrefix))
|
.filter((pageMeta) => pageMeta.name.startsWith(this.trashPrefix))
|
||||||
.map((pageMeta) => ({
|
.map((pageMeta) => ({
|
||||||
...pageMeta,
|
...pageMeta,
|
||||||
|
@ -56,27 +63,28 @@ export class SpaceSync {
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
let syncOps = 0;
|
let syncOps = 0;
|
||||||
|
|
||||||
let allPagesPrimary = new Map(
|
let { pages: primaryAllPagesSet, nowTimestamp: primarySyncTimestamp } =
|
||||||
(await this.syncablePages(this.primary)).map((p) => [p.name, p])
|
await this.syncablePages(this.primary);
|
||||||
);
|
let allPagesPrimary = new Map(primaryAllPagesSet.map((p) => [p.name, p]));
|
||||||
|
let { pages: secondaryAllPagesSet, nowTimestamp: secondarySyncTimestamp } =
|
||||||
|
await this.syncablePages(this.secondary);
|
||||||
let allPagesSecondary = new Map(
|
let allPagesSecondary = new Map(
|
||||||
(await this.syncablePages(this.secondary)).map((p) => [p.name, p])
|
secondaryAllPagesSet.map((p) => [p.name, p])
|
||||||
);
|
);
|
||||||
|
|
||||||
let allTrashPrimary = new Map(
|
let allTrashPrimary = new Map(
|
||||||
(await this.trashPages(this.primary))
|
(await this.trashPages(this.primary))
|
||||||
// Filter out old trash
|
// Filter out old trash
|
||||||
.filter((p) => p.lastModified > this.lastSync)
|
.filter((p) => p.lastModified > this.primaryLastSync)
|
||||||
.map((p) => [p.name, p])
|
.map((p) => [p.name, p])
|
||||||
);
|
);
|
||||||
let allTrashSecondary = new Map(
|
let allTrashSecondary = new Map(
|
||||||
(await this.trashPages(this.secondary))
|
(await this.trashPages(this.secondary))
|
||||||
// Filter out old trash
|
// Filter out old trash
|
||||||
.filter((p) => p.lastModified > this.lastSync)
|
.filter((p) => p.lastModified > this.secondaryLastSync)
|
||||||
.map((p) => [p.name, p])
|
.map((p) => [p.name, p])
|
||||||
);
|
);
|
||||||
|
|
||||||
let createdPagesOnSecondary = new Set<string>();
|
|
||||||
|
|
||||||
// Iterate over all pages on the primary first
|
// Iterate over all pages on the primary first
|
||||||
for (let [name, pageMetaPrimary] of allPagesPrimary.entries()) {
|
for (let [name, pageMetaPrimary] of allPagesPrimary.entries()) {
|
||||||
let pageMetaSecondary = allPagesSecondary.get(pageMetaPrimary.name);
|
let pageMetaSecondary = allPagesSecondary.get(pageMetaPrimary.name);
|
||||||
|
@ -95,15 +103,14 @@ export class SpaceSync {
|
||||||
name,
|
name,
|
||||||
pageData.text,
|
pageData.text,
|
||||||
true,
|
true,
|
||||||
pageData.meta.lastModified
|
secondarySyncTimestamp // The reason for this is to not include it in the next sync cycle, we cannot blindly use the lastModified date due to time skew
|
||||||
);
|
);
|
||||||
syncOps++;
|
syncOps++;
|
||||||
createdPagesOnSecondary.add(name);
|
|
||||||
} else {
|
} else {
|
||||||
// Existing page
|
// Existing page
|
||||||
if (pageMetaPrimary.lastModified > this.lastSync) {
|
if (pageMetaPrimary.lastModified > this.primaryLastSync) {
|
||||||
// Primary updated since last sync
|
// Primary updated since last sync
|
||||||
if (pageMetaSecondary.lastModified > this.lastSync) {
|
if (pageMetaSecondary.lastModified > this.secondaryLastSync) {
|
||||||
// Secondary also updated! CONFLICT
|
// Secondary also updated! CONFLICT
|
||||||
if (conflictResolver) {
|
if (conflictResolver) {
|
||||||
await conflictResolver(pageMetaPrimary, pageMetaSecondary);
|
await conflictResolver(pageMetaPrimary, pageMetaSecondary);
|
||||||
|
@ -124,11 +131,11 @@ export class SpaceSync {
|
||||||
name,
|
name,
|
||||||
pageData.text,
|
pageData.text,
|
||||||
false,
|
false,
|
||||||
pageData.meta.lastModified
|
secondarySyncTimestamp
|
||||||
);
|
);
|
||||||
syncOps++;
|
syncOps++;
|
||||||
}
|
}
|
||||||
} else if (pageMetaSecondary.lastModified > this.lastSync) {
|
} else if (pageMetaSecondary.lastModified > this.secondaryLastSync) {
|
||||||
// Secondary updated, but not primary (checked above)
|
// Secondary updated, but not primary (checked above)
|
||||||
// Push from secondary to primary
|
// Push from secondary to primary
|
||||||
console.log("Changed page on secondary", name, "syncing to primary");
|
console.log("Changed page on secondary", name, "syncing to primary");
|
||||||
|
@ -137,7 +144,7 @@ export class SpaceSync {
|
||||||
name,
|
name,
|
||||||
pageData.text,
|
pageData.text,
|
||||||
false,
|
false,
|
||||||
pageData.meta.lastModified
|
primarySyncTimestamp
|
||||||
);
|
);
|
||||||
syncOps++;
|
syncOps++;
|
||||||
} else {
|
} else {
|
||||||
|
@ -161,8 +168,8 @@ export class SpaceSync {
|
||||||
await this.primary.writePage(
|
await this.primary.writePage(
|
||||||
name,
|
name,
|
||||||
pageData.text,
|
pageData.text,
|
||||||
true,
|
false,
|
||||||
pageData.meta.lastModified
|
primarySyncTimestamp
|
||||||
);
|
);
|
||||||
syncOps++;
|
syncOps++;
|
||||||
}
|
}
|
||||||
|
@ -170,50 +177,45 @@ export class SpaceSync {
|
||||||
|
|
||||||
// And finally, let's trash some pages
|
// And finally, let's trash some pages
|
||||||
for (let pageToDelete of allTrashPrimary.values()) {
|
for (let pageToDelete of allTrashPrimary.values()) {
|
||||||
if (pageToDelete.lastModified > this.lastSync) {
|
console.log("Deleting", pageToDelete.name, "on secondary");
|
||||||
// New deletion
|
try {
|
||||||
console.log("Deleting", pageToDelete.name, "on secondary");
|
await this.secondary.deletePage(
|
||||||
try {
|
pageToDelete.name,
|
||||||
await this.secondary.deletePage(
|
secondarySyncTimestamp
|
||||||
pageToDelete.name,
|
);
|
||||||
pageToDelete.lastModified
|
syncOps++;
|
||||||
);
|
} catch (e: any) {
|
||||||
syncOps++;
|
console.log("Page already gone", e.message);
|
||||||
} catch (e: any) {
|
|
||||||
console.log("Page already gone", e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let pageToDelete of allTrashSecondary.values()) {
|
for (let pageToDelete of allTrashSecondary.values()) {
|
||||||
if (pageToDelete.lastModified > this.lastSync) {
|
console.log("Deleting", pageToDelete.name, "on primary");
|
||||||
// New deletion
|
try {
|
||||||
console.log("Deleting", pageToDelete.name, "on primary");
|
await this.primary.deletePage(pageToDelete.name, primarySyncTimestamp);
|
||||||
try {
|
syncOps++;
|
||||||
await this.primary.deletePage(
|
} catch (e: any) {
|
||||||
pageToDelete.name,
|
console.log("Page already gone", e.message);
|
||||||
pageToDelete.lastModified
|
|
||||||
);
|
|
||||||
syncOps++;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.log("Page already gone", e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setting last sync time to the timestamps we got back when fetching the page lists on each end
|
||||||
|
this.primaryLastSync = primarySyncTimestamp;
|
||||||
|
this.secondaryLastSync = secondarySyncTimestamp;
|
||||||
|
|
||||||
// Find the latest timestamp and set it as lastSync
|
// Find the latest timestamp and set it as lastSync
|
||||||
allPagesPrimary.forEach((pageMeta) => {
|
// allPagesPrimary.forEach((pageMeta) => {
|
||||||
this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
|
// this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
|
||||||
});
|
// });
|
||||||
allPagesSecondary.forEach((pageMeta) => {
|
// allPagesSecondary.forEach((pageMeta) => {
|
||||||
this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
|
// this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
|
||||||
});
|
// });
|
||||||
allTrashPrimary.forEach((pageMeta) => {
|
// allTrashPrimary.forEach((pageMeta) => {
|
||||||
this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
|
// this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
|
||||||
});
|
// });
|
||||||
allTrashSecondary.forEach((pageMeta) => {
|
// allTrashSecondary.forEach((pageMeta) => {
|
||||||
this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
|
// this.lastSync = Math.max(this.lastSync, pageMeta.lastModified);
|
||||||
});
|
// });
|
||||||
|
|
||||||
return syncOps;
|
return syncOps;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Space } from "../spaces/space";
|
|
||||||
import { SysCallMapping } from "../../plugos/system";
|
import { SysCallMapping } from "../../plugos/system";
|
||||||
import { proxySyscalls } from "../../plugos/syscalls/transport";
|
import { proxySyscalls } from "../../plugos/syscalls/transport";
|
||||||
|
import { WatchableSpace } from "../spaces/cache_space";
|
||||||
|
|
||||||
export function indexerSyscalls(space: Space): SysCallMapping {
|
export function indexerSyscalls(space: WatchableSpace): SysCallMapping {
|
||||||
return proxySyscalls(
|
return proxySyscalls(
|
||||||
[
|
[
|
||||||
"index.scanPrefixForPage",
|
"index.scanPrefixForPage",
|
|
@ -1,7 +1,7 @@
|
||||||
import { SysCallMapping } from "../../plugos/system";
|
import { SysCallMapping } from "../../plugos/system";
|
||||||
import { Space } from "../spaces/space";
|
import { WatchableSpace } from "../spaces/cache_space";
|
||||||
|
|
||||||
export function systemSyscalls(space: Space): SysCallMapping {
|
export function systemSyscalls(space: WatchableSpace): SysCallMapping {
|
||||||
return {
|
return {
|
||||||
"system.invokeFunction": async (
|
"system.invokeFunction": async (
|
||||||
ctx,
|
ctx,
|
||||||
|
|
Loading…
Reference in New Issue