parent
3fb393baf3
commit
2577a2db32
|
@ -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";
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]);
|
|
@ -23,3 +23,10 @@ functions:
|
|||
performSync:
|
||||
env: server
|
||||
path: sync.ts:performSync
|
||||
cron: "* * * * *"
|
||||
|
||||
syncPage:
|
||||
path: sync.ts:syncPage
|
||||
env: server
|
||||
events:
|
||||
- page:saved
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -27,7 +27,10 @@ Here’s how to use SilverBullet’s 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 doesn’t make any assumptions about clocks being in sync, timezones etc.
|
||||
|
|
Loading…
Reference in New Issue