1
0
silverbullet/common/syscalls/sync.ts

136 lines
3.9 KiB
TypeScript
Raw Normal View History

2023-01-14 17:51:00 +00:00
import { SysCallMapping, System } from "../../plugos/system.ts";
2023-01-13 14:41:29 +00:00
import type { SyncEndpoint } from "../../plug-api/silverbullet-syscall/sync.ts";
import { SpaceSync, SyncStatusItem } from "../spaces/sync.ts";
import { HttpSpacePrimitives } from "../spaces/http_space_primitives.ts";
import { SpacePrimitives } from "../spaces/space_primitives.ts";
2023-01-14 17:51:00 +00:00
import { race, timeout } from "../async_util.ts";
2023-01-13 14:41:29 +00:00
2023-01-14 17:51:00 +00:00
export function syncSyscalls(
localSpace: SpacePrimitives,
system: System<any>,
): SysCallMapping {
2023-01-13 14:41:29 +00:00
return {
"sync.syncAll": async (
2023-01-13 14:41:29 +00:00
_ctx,
endpoint: SyncEndpoint,
snapshot: Record<string, SyncStatusItem>,
): Promise<
{
snapshot: Record<string, SyncStatusItem>;
operations: number;
// The reason to not just throw an Error is so that the partially updated snapshot can still be saved
error?: string;
}
> => {
const { spaceSync } = setupSync(endpoint, snapshot);
2023-01-13 14:41:29 +00:00
try {
const operations = await spaceSync.syncFiles(
SpaceSync.primaryConflictResolver,
);
return {
// And convert back to JSON
snapshot: Object.fromEntries(spaceSync.snapshot),
operations,
};
} catch (e: any) {
return {
snapshot: Object.fromEntries(spaceSync.snapshot),
operations: -1,
error: e.message,
};
}
},
"sync.syncFile": async (
_ctx,
endpoint: SyncEndpoint,
snapshot: Record<string, SyncStatusItem>,
name: string,
): Promise<
{
snapshot: Record<string, SyncStatusItem>;
operations: number;
// The reason to not just throw an Error is so that the partially updated snapshot can still be saved
error?: string;
}
> => {
const { spaceSync, remoteSpace } = setupSync(endpoint, snapshot);
try {
const localHash = (await localSpace.getFileMeta(name)).lastModified;
let remoteHash: number | undefined = undefined;
try {
remoteHash =
(await race([remoteSpace.getFileMeta(name), timeout(1000)]))
.lastModified;
} catch (e: any) {
if (e.message.includes("File not found")) {
// File doesn't exist remotely, that's ok
} else {
throw e;
}
}
const operations = await spaceSync.syncFile(
name,
localHash,
remoteHash,
SpaceSync.primaryConflictResolver,
);
return {
// And convert back to JSON
snapshot: Object.fromEntries(spaceSync.snapshot),
operations,
};
} catch (e: any) {
return {
snapshot: Object.fromEntries(spaceSync.snapshot),
operations: -1,
error: e.message,
};
}
},
2023-01-13 14:41:29 +00:00
"sync.check": async (_ctx, endpoint: SyncEndpoint): Promise<void> => {
const syncSpace = new HttpSpacePrimitives(
endpoint.url,
endpoint.user,
endpoint.password,
);
// Let's just fetch metadata for the SETTINGS.md file (which should always exist)
2023-01-14 17:51:00 +00:00
try {
await race([
syncSpace.getFileMeta("SETTINGS.md"),
timeout(2000),
]);
2023-01-14 17:51:00 +00:00
} catch (e: any) {
console.error("Sync check failure", e.message);
throw e;
}
2023-01-13 14:41:29 +00:00
},
};
function setupSync(
endpoint: SyncEndpoint,
snapshot: Record<string, SyncStatusItem>,
) {
const remoteSpace = new HttpSpacePrimitives(
endpoint.url,
endpoint.user,
endpoint.password,
// Base64 PUTs to support mobile
true,
);
// Convert from JSON to a Map
const syncStatusMap = new Map<string, SyncStatusItem>(
Object.entries(snapshot),
);
const spaceSync = new SpaceSync(
localSpace,
remoteSpace,
syncStatusMap,
// Log to the "sync" plug sandbox
system.loadedPlugs.get("sync")!.sandbox!,
);
return { spaceSync, remoteSpace };
}
2023-01-13 14:41:29 +00:00
}