Continuous sync (#320)

* Continuous sync

* Mobile dep upgrade
pull/321/head
Zef Hemel 2023-01-20 16:08:01 +01:00 committed by GitHub
parent 3fb393baf3
commit 2577a2db32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 417 additions and 225 deletions

View File

@ -1,6 +1,6 @@
import * as plugos from "../plugos/types.ts";
import { EndpointHookT } from "../plugos/hooks/endpoint.ts";
import { CronHookT } from "../plugos/hooks/cron.deno.ts";
import { CronHookT } from "../plugos/hooks/cron.ts";
import { EventHookT } from "../plugos/hooks/event.ts";
import { CommandHookT } from "../web/hooks/command.ts";
import { SlashCommandHookT } from "../web/hooks/slash_command.ts";

View File

@ -130,9 +130,12 @@ Deno.test("Test store", async () => {
ternary,
new Map<string, SyncStatusItem>(),
);
console.log("N ops", await sync2.syncFiles());
console.log(
"N ops",
await sync2.syncFiles(SpaceSync.primaryConflictResolver),
);
await sleep(2);
assertEquals(await sync2.syncFiles(), 0);
assertEquals(await sync2.syncFiles(SpaceSync.primaryConflictResolver), 0);
await Deno.remove(primaryPath, { recursive: true });
await Deno.remove(secondaryPath, { recursive: true });

View File

@ -30,7 +30,7 @@ export class SpaceSync {
) {}
async syncFiles(
conflictResolver?: (
conflictResolver: (
name: string,
snapshot: Map<string, SyncStatusItem>,
primarySpace: SpacePrimitives,
@ -65,8 +65,38 @@ export class SpaceSync {
this.logger.log("info", "Iterating over all files");
for (const name of allFilesToProcess) {
operations += await this.syncFile(
name,
primaryFileMap.get(name),
secondaryFileMap.get(name),
conflictResolver,
);
}
} catch (e: any) {
this.logger.log("error", "Sync error:", e.message);
throw e;
}
this.logger.log("info", "Sync complete, operations performed", operations);
return operations;
}
async syncFile(
name: string,
primaryHash: SyncHash | undefined,
secondaryHash: SyncHash | undefined,
conflictResolver: (
name: string,
snapshot: Map<string, SyncStatusItem>,
primarySpace: SpacePrimitives,
secondarySpace: SpacePrimitives,
logger: Logger,
) => Promise<number>,
): Promise<number> {
let operations = 0;
if (
primaryFileMap.has(name) && !secondaryFileMap.has(name) &&
primaryHash && !secondaryHash &&
!this.snapshot.has(name)
) {
// New file, created on primary, copy from primary to secondary
@ -82,12 +112,12 @@ export class SpaceSync {
data,
);
this.snapshot.set(name, [
primaryFileMap.get(name)!,
primaryHash,
writtenMeta.lastModified,
]);
operations++;
} else if (
secondaryFileMap.has(name) && !primaryFileMap.has(name) &&
secondaryHash && !primaryHash &&
!this.snapshot.has(name)
) {
// New file, created on secondary, copy from secondary to primary
@ -104,12 +134,12 @@ export class SpaceSync {
);
this.snapshot.set(name, [
writtenMeta.lastModified,
secondaryFileMap.get(name)!,
secondaryHash,
]);
operations++;
} else if (
primaryFileMap.has(name) && this.snapshot.has(name) &&
!secondaryFileMap.has(name)
primaryHash && this.snapshot.has(name) &&
!secondaryHash
) {
// File deleted on B
this.logger.log(
@ -121,8 +151,8 @@ export class SpaceSync {
this.snapshot.delete(name);
operations++;
} else if (
secondaryFileMap.has(name) && this.snapshot.has(name) &&
!primaryFileMap.has(name)
secondaryHash && this.snapshot.has(name) &&
!primaryHash
) {
// File deleted on A
this.logger.log(
@ -134,8 +164,8 @@ export class SpaceSync {
this.snapshot.delete(name);
operations++;
} else if (
this.snapshot.has(name) && !primaryFileMap.has(name) &&
!secondaryFileMap.has(name)
this.snapshot.has(name) && !primaryHash &&
!secondaryHash
) {
// File deleted on both sides, :shrug:
this.logger.log(
@ -146,10 +176,10 @@ export class SpaceSync {
this.snapshot.delete(name);
operations++;
} else if (
primaryFileMap.has(name) && secondaryFileMap.has(name) &&
primaryHash && secondaryHash &&
this.snapshot.get(name) &&
primaryFileMap.get(name) !== this.snapshot.get(name)![0] &&
secondaryFileMap.get(name) === this.snapshot.get(name)![1]
primaryHash !== this.snapshot.get(name)![0] &&
secondaryHash === this.snapshot.get(name)![1]
) {
// File has changed on primary, but not secondary: copy from primary to secondary
this.logger.log(
@ -164,15 +194,15 @@ export class SpaceSync {
data,
);
this.snapshot.set(name, [
primaryFileMap.get(name)!,
primaryHash,
writtenMeta.lastModified,
]);
operations++;
} else if (
primaryFileMap.has(name) && secondaryFileMap.has(name) &&
primaryHash && secondaryHash &&
this.snapshot.get(name) &&
secondaryFileMap.get(name) !== this.snapshot.get(name)![1] &&
primaryFileMap.get(name) === this.snapshot.get(name)![0]
secondaryHash !== this.snapshot.get(name)![1] &&
primaryHash === this.snapshot.get(name)![0]
) {
// File has changed on secondary, but not primary: copy from secondary to primary
const { data } = await this.secondary.readFile(name, "arraybuffer");
@ -183,19 +213,19 @@ export class SpaceSync {
);
this.snapshot.set(name, [
writtenMeta.lastModified,
secondaryFileMap.get(name)!,
secondaryHash,
]);
operations++;
} 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
primaryFileMap.has(name) && secondaryFileMap.has(name) &&
primaryHash && secondaryHash &&
!this.snapshot.has(name)
) ||
( // File changed on both ends, CONFLICT!
primaryFileMap.has(name) && secondaryFileMap.has(name) &&
primaryHash && secondaryHash &&
this.snapshot.get(name) &&
secondaryFileMap.get(name) !== this.snapshot.get(name)![1] &&
primaryFileMap.get(name) !== this.snapshot.get(name)![0]
secondaryHash !== this.snapshot.get(name)![1] &&
primaryHash !== this.snapshot.get(name)![0]
)
) {
this.logger.log(
@ -203,7 +233,6 @@ export class SpaceSync {
"File changed on both ends, potential conflict",
name,
);
if (conflictResolver) {
operations += await conflictResolver(
name,
this.snapshot,
@ -211,21 +240,9 @@ export class SpaceSync {
this.secondary,
this.logger,
);
} else {
throw Error(
`Sync conflict for ${name} with no conflict resolver specified`,
);
}
} else {
// Nothing needs to happen
}
}
} catch (e: any) {
this.logger.log("error", "Sync error:", e.message);
throw e;
}
this.logger.log("info", "Sync complete, operations performed", operations);
return operations;
}

View File

@ -10,7 +10,7 @@ export function syncSyscalls(
system: System<any>,
): SysCallMapping {
return {
"sync.sync": async (
"sync.syncAll": async (
_ctx,
endpoint: SyncEndpoint,
snapshot: Record<string, SyncStatusItem>,
@ -22,24 +22,7 @@ export function syncSyscalls(
error?: string;
}
> => {
const syncSpace = 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,
syncSpace,
syncStatusMap,
// Log to the "sync" plug sandbox
system.loadedPlugs.get("sync")!.sandbox!,
);
const { spaceSync } = setupSync(endpoint, snapshot);
try {
const operations = await spaceSync.syncFiles(
@ -58,19 +41,95 @@ export function syncSyscalls(
};
}
},
"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,
};
}
},
"sync.check": async (_ctx, endpoint: SyncEndpoint): Promise<void> => {
const syncSpace = new HttpSpacePrimitives(
endpoint.url,
endpoint.user,
endpoint.password,
);
// Let's just fetch the file list to see if it works with a timeout of 5s
// Let's just fetch metadata for the SETTINGS.md file (which should always exist)
try {
await race([syncSpace.fetchFileList(), timeout(5000)]);
await race([
syncSpace.getFileMeta("SETTINGS.md"),
timeout(2000),
]);
} catch (e: any) {
console.error("Sync check failure", e.message);
throw e;
}
},
};
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 };
}
}

View File

@ -1,12 +1,13 @@
{
"tasks": {
"clean": "rm -rf dist dist_bundle",
"install": "deno install -f -A --unstable silverbullet.ts",
"install": "deno install -f -A --unstable --importmap import_map.json silverbullet.ts",
"check": "find web common server plugs cmd plug-api plugos -name '*.ts*' | xargs deno check",
"test": "deno test -A --unstable",
"build": "deno run -A --unstable build_plugs.ts && deno run -A --unstable build_web.ts",
"plugs": "deno run -A --unstable build_plugs.ts",
"watch-web": "deno run -A --unstable --check build_web.ts --watch",
"server": "deno run -A --unstable --check silverbullet.ts",
"watch-server": "deno run -A --unstable --check --watch silverbullet.ts",
// The only reason to run a shell script is that deno task doesn't support globs yet (e.g. *.plug.yaml)
"watch-plugs": "deno run -A --unstable --check build_plugs.ts -w",

View File

@ -19,7 +19,7 @@
"@lezer/lr": "https://esm.sh/@lezer/lr@1.2.5?external=@lezer/common",
"yaml": "https://deno.land/std/encoding/yaml.ts",
"@capacitor/core": "https://esm.sh/@capacitor/core@4.6.1",
"@capacitor/core": "https://esm.sh/@capacitor/core@4.6.2",
"@capacitor/filesystem": "https://esm.sh/@capacitor/filesystem@4.1.4?external=@capacitor/core"
}
}

View File

@ -6,7 +6,7 @@ import { PageNamespaceHook } from "../common/hooks/page_namespace.ts";
import { SilverBulletHooks } from "../common/manifest.ts";
import { System } from "../plugos/system.ts";
import { BuiltinSettings } from "../web/types.ts";
import { Directory } from "./deps.ts";
import { CapacitorHttp, Directory } from "./deps.ts";
import { CapacitorSpacePrimitives } from "./spaces/capacitor_space_primitives.ts";
import { AssetBundlePlugSpacePrimitives } from "../common/spaces/asset_bundle_space_primitives.ts";
@ -31,6 +31,7 @@ import { EventHook } from "../plugos/hooks/event.ts";
import { clientStoreSyscalls } from "./syscalls/clientStore.ts";
import { sandboxFetchSyscalls } from "../plugos/syscalls/fetch.ts";
import { syncSyscalls } from "../common/syscalls/sync.ts";
import { CronHook } from "../plugos/hooks/cron.ts";
safeRun(async () => {
// Instantiate a PlugOS system for the client
@ -47,6 +48,8 @@ safeRun(async () => {
const db = new CapacitorDb("data.db");
await db.init();
system.addHook(new CronHook());
// for store
await ensureStoreTable(db, "store");
// for clientStore

View File

@ -1,3 +1,3 @@
export { Capacitor } from "@capacitor/core";
export { Capacitor, CapacitorHttp } from "@capacitor/core";
export { Directory, Encoding, Filesystem } from "@capacitor/filesystem";
export type { WriteFileResult } from "@capacitor/filesystem";

View File

@ -10,11 +10,11 @@
"license": "ISC",
"dependencies": {
"@capacitor-community/sqlite": "^4.6.0",
"@capacitor/android": "^4.6.1",
"@capacitor/android": "^4.6.2",
"@capacitor/app": "^4.1.1",
"@capacitor/core": "latest",
"@capacitor/core": "^4.6.2",
"@capacitor/filesystem": "^4.1.4",
"@capacitor/ios": "^4.6.1",
"@capacitor/ios": "^4.6.2",
"@capacitor/keyboard": "^4.1.0",
"@capacitor/splash-screen": "latest",
"cordova-res": "^0.15.4"
@ -36,9 +36,9 @@
}
},
"node_modules/@capacitor/android": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-4.6.1.tgz",
"integrity": "sha512-Hnh1tmUr1SP67U6D6ry5I5BEBSN/1nkBAIjQIqf5tF82WNxKbpbC6GfkHE4hMJZinRTrCf36LkrdP8srh7SxoA==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-4.6.2.tgz",
"integrity": "sha512-PQpOJnMi0i/d4UrT8bPdfkwlKAlQLgsyo2YKj+iUYjEIu8sKQvqDirLYnpeKhj4cflIG2u9mh/eFncooA+u2gw==",
"peerDependencies": {
"@capacitor/core": "^4.6.0"
}
@ -84,9 +84,9 @@
}
},
"node_modules/@capacitor/core": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-4.6.1.tgz",
"integrity": "sha512-7A2IV9E8umgu9u0fChUTjQJq+Jp25GJZMmWxoQN/nVx/1rcpFJ4m1xo3NPBoIRs+aV7FR+BM17mPrnkKlA8N2g==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-4.6.2.tgz",
"integrity": "sha512-M/KpAg+peft/HTb7svLiKHxjbll67ybs1vEqhZuvjXlwro53NxNXR4YJS7+wNXZSiA4Kxjtf+a754xGgZcMarA==",
"dependencies": {
"tslib": "^2.1.0"
}
@ -100,9 +100,9 @@
}
},
"node_modules/@capacitor/ios": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-4.6.1.tgz",
"integrity": "sha512-kH1nPG2jCk7w6ASf2VX+tIxHoc2Z/c5+7d89yvtiKmEZXoPLuVyAv/Yx4PhJP2r7KSyl5S2gZZkzQrMdAjDVKg==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-4.6.2.tgz",
"integrity": "sha512-3hQzbAOk+drCLyFjnytvkc20Mr077/9tQrv6iTghDXESDGR6EgcaYUXzKdVwuJscb0R459+5UQ2mYtkx6ES4TQ==",
"peerDependencies": {
"@capacitor/core": "^4.6.0"
}
@ -2199,9 +2199,9 @@
}
},
"@capacitor/android": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-4.6.1.tgz",
"integrity": "sha512-Hnh1tmUr1SP67U6D6ry5I5BEBSN/1nkBAIjQIqf5tF82WNxKbpbC6GfkHE4hMJZinRTrCf36LkrdP8srh7SxoA==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-4.6.2.tgz",
"integrity": "sha512-PQpOJnMi0i/d4UrT8bPdfkwlKAlQLgsyo2YKj+iUYjEIu8sKQvqDirLYnpeKhj4cflIG2u9mh/eFncooA+u2gw==",
"requires": {}
},
"@capacitor/app": {
@ -2236,9 +2236,9 @@
}
},
"@capacitor/core": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-4.6.1.tgz",
"integrity": "sha512-7A2IV9E8umgu9u0fChUTjQJq+Jp25GJZMmWxoQN/nVx/1rcpFJ4m1xo3NPBoIRs+aV7FR+BM17mPrnkKlA8N2g==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-4.6.2.tgz",
"integrity": "sha512-M/KpAg+peft/HTb7svLiKHxjbll67ybs1vEqhZuvjXlwro53NxNXR4YJS7+wNXZSiA4Kxjtf+a754xGgZcMarA==",
"requires": {
"tslib": "^2.1.0"
}
@ -2250,9 +2250,9 @@
"requires": {}
},
"@capacitor/ios": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-4.6.1.tgz",
"integrity": "sha512-kH1nPG2jCk7w6ASf2VX+tIxHoc2Z/c5+7d89yvtiKmEZXoPLuVyAv/Yx4PhJP2r7KSyl5S2gZZkzQrMdAjDVKg==",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-4.6.2.tgz",
"integrity": "sha512-3hQzbAOk+drCLyFjnytvkc20Mr077/9tQrv6iTghDXESDGR6EgcaYUXzKdVwuJscb0R459+5UQ2mYtkx6ES4TQ==",
"requires": {}
},
"@capacitor/keyboard": {

View File

@ -14,11 +14,11 @@
},
"dependencies": {
"@capacitor-community/sqlite": "^4.6.0",
"@capacitor/android": "^4.6.1",
"@capacitor/android": "^4.6.2",
"@capacitor/app": "^4.1.1",
"@capacitor/core": "latest",
"@capacitor/core": "^4.6.2",
"@capacitor/filesystem": "^4.1.4",
"@capacitor/ios": "^4.6.1",
"@capacitor/ios": "^4.6.2",
"@capacitor/keyboard": "^4.1.0",
"@capacitor/splash-screen": "latest",
"cordova-res": "^0.15.4"

View File

@ -9,7 +9,7 @@ export type SyncEndpoint = {
// Perform a sync with the server, based on the given status (to be persisted)
// returns a new sync status to persist
export function sync(
export function syncAll(
endpoint: SyncEndpoint,
snapshot: Record<string, SyncStatusItem>,
): Promise<
@ -19,7 +19,23 @@ export function sync(
error?: string;
}
> {
return syscall("sync.sync", endpoint, snapshot);
return syscall("sync.syncAll", endpoint, snapshot);
}
// Perform a sync with the server, based on the given status (to be persisted)
// returns a new sync status to persist
export function syncFile(
endpoint: SyncEndpoint,
snapshot: Record<string, SyncStatusItem>,
name: string,
): Promise<
{
snapshot: Record<string, SyncStatusItem>;
operations: number;
error?: string;
}
> {
return syscall("sync.syncFile", endpoint, snapshot, name);
}
// Checks the sync endpoint for connectivity and authentication, throws and Error on failure

View File

@ -7,7 +7,7 @@ export type CronHookT = {
cron?: string | string[];
};
export class DenoCronHook implements Hook<CronHookT> {
export class CronHook implements Hook<CronHookT> {
apply(system: System<CronHookT>): void {
let tasks: Cron[] = [];
system.on({
@ -42,7 +42,7 @@ export class DenoCronHook implements Hook<CronHookT> {
for (const cronDef of crons) {
tasks.push(
new Cron(cronDef, () => {
console.log("Now acting on cron", cronDef);
// console.log("Now acting on cron", cronDef);
safeRun(async () => {
try {
await plug.invoke(name, [cronDef]);

View File

@ -23,3 +23,10 @@ functions:
performSync:
env: server
path: sync.ts:performSync
cron: "* * * * *"
syncPage:
path: sync.ts:syncPage
env: server
events:
- page:saved

View File

@ -125,18 +125,96 @@ export function check(config: SyncEndpoint) {
return sync.check(config);
}
// const syncTimeout = 1000 * 60 * 30; // 30 minutes
const syncTimeout = 1000 * 20; // 20s
// Run on server
export async function performSync() {
const config: SyncEndpoint = await store.get("sync.config");
if (!config) {
// Sync not configured
return;
}
try {
await sync.check(config);
} catch (e: any) {
console.error("Sync check failed", e.message);
return;
}
// Check if sync not already in progress
const ongoingSync: number | undefined = await store.get("sync.startTime");
if (ongoingSync) {
if (Date.now() - ongoingSync > syncTimeout) {
console.log("Sync timed out, continuing");
} else {
console.log("Sync already in progress");
return;
}
}
// Keep track of sync start time
await store.set("sync.startTime", Date.now());
try {
// Perform actual sync
const snapshot = await store.get("sync.snapshot");
const { snapshot: newSnapshot, operations, error } = await sync.sync(
const { snapshot: newSnapshot, operations, error } = await sync.syncAll(
config,
snapshot,
);
// Store snapshot
await store.set("sync.snapshot", newSnapshot);
// Clear sync start time
await store.del("sync.startTime");
if (error) {
console.error("Sync error", error);
throw new Error(error);
}
return operations;
} catch (e: any) {
// Clear sync start time
await store.del("sync.startTime");
console.error("Sync error", e);
}
}
export async function syncPage(page: string) {
const config: SyncEndpoint = await store.get("sync.config");
if (!config) {
// Sync not configured
return;
}
// Check if sync not already in progress
const ongoingSync: number | undefined = await store.get("sync.startTime");
if (ongoingSync) {
if (Date.now() - ongoingSync > syncTimeout) {
console.log("Sync timed out, continuing");
} else {
console.log("Sync already in progress");
return;
}
}
// Keep track of sync start time
await store.set("sync.startTime", Date.now());
const snapshot = await store.get("sync.snapshot");
console.log("Syncing page", page);
try {
const { snapshot: newSnapshot, error } = await sync.syncFile(
config,
snapshot,
`${page}.md`,
);
// Store snapshot
await store.set("sync.snapshot", newSnapshot);
// Clear sync start time
await store.del("sync.startTime");
if (error) {
console.error("Sync error", error);
throw new Error(error);
}
} catch (e: any) {
// Clear sync start time
await store.del("sync.startTime");
console.error("Sync error", e);
}
}

View File

@ -8,7 +8,7 @@ import { SpacePrimitives } from "../common/spaces/space_primitives.ts";
import { markdownSyscalls } from "../common/syscalls/markdown.ts";
import { createSandbox } from "../plugos/environments/deno_sandbox.ts";
import { EventHook } from "../plugos/hooks/event.ts";
import { DenoCronHook } from "../plugos/hooks/cron.deno.ts";
import { CronHook } from "../plugos/hooks/cron.ts";
import { esbuildSyscalls } from "../plugos/syscalls/esbuild.ts";
import { eventSyscalls } from "../plugos/syscalls/event.ts";
import fileSystemSyscalls from "../plugos/syscalls/fs.deno.ts";
@ -103,7 +103,7 @@ export class SpaceSystem {
}
// The cron hook
this.system.addHook(new DenoCronHook());
this.system.addHook(new CronHook());
// Register syscalls available on the server side
this.system.registerSyscalls(

View File

@ -1,8 +1,13 @@
An attempt at documenting the changes/new features introduced in each
release.
---
---
## Next
* Fixed copy & paste, drag & drop of attachments in the [[Desktop]] app
* Continuous [[Sync]]
---
## 0.2.8
* [[Sync]] should now be usable and is documented
* Windows and Mac [[Desktop]] apps now have proper icons (only Linux left)

View File

@ -27,7 +27,10 @@ Heres how to use SilverBullets sync functionality:
2. Use {[Sync: Sync]} to perform a regular sync, comparing the local and remote space and generating conflicts where appropriate.
3. Check {[Show Logs]} for sync logs.
After this initial sync, **sync needs to be triggered manually**, so run {[Sync: Sync]} whenever you feel a sync is warranted.
Sync is triggered:
* Continuously when changes are made to a page in a client set up with sync, immediately after the page persists (single file sync)
* Automatically every minute (full space sync)
* Manually using the {[Sync: Sync]} command (full space sync)
## The sync process
1. The sync engine compares two file listings: the local one and remote one, and figures out which files have been added, changed and removed on both ends. It uses timestamps to determine changes. Note this doesnt make any assumptions about clocks being in sync, timezones etc.