monoidentity 0.20.1 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Monoidentity.svelte +7 -1
- package/dist/Monoidentity.svelte.d.ts +1 -1
- package/dist/readyup.d.ts +1 -1
- package/dist/readyup.js +26 -5
- package/dist/storage/_backupcloud.d.ts +3 -0
- package/dist/storage/_backupcloud.js +23 -0
- package/dist/storage/backupcloud-connection.d.ts +3 -0
- package/dist/storage/backupcloud-connection.js +8 -0
- package/dist/storage/backupcloud-pull.d.ts +6 -0
- package/dist/storage/backupcloud-pull.js +92 -0
- package/dist/storage/backupcloud-push.d.ts +4 -0
- package/dist/storage/backupcloud-push.js +57 -0
- package/dist/storage/backuplocally-pull.d.ts +1 -0
- package/dist/storage/backuplocally-pull.js +58 -0
- package/dist/storage/backuplocally-push.d.ts +2 -0
- package/dist/storage/backuplocally-push.js +63 -0
- package/dist/storage/utils-sync.js +3 -1
- package/dist/verification/use-studentvue-remote.js +2 -0
- package/package.json +14 -14
- package/dist/storage/backupcloud.d.ts +0 -3
- package/dist/storage/backupcloud.js +0 -157
- package/dist/storage/backuplocally.d.ts +0 -2
- package/dist/storage/backuplocally.js +0 -112
package/dist/Monoidentity.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type
|
|
2
|
+
import { onDestroy, type Snippet } from "svelte";
|
|
3
3
|
import { readyUp } from "./readyup.js";
|
|
4
4
|
import type { Intent } from "./utils-transport.js";
|
|
5
5
|
import type { SyncStrategy } from "./storage/utils-storage.js";
|
|
@@ -16,11 +16,17 @@
|
|
|
16
16
|
children: Snippet;
|
|
17
17
|
} = $props();
|
|
18
18
|
|
|
19
|
+
const aborter = new AbortController();
|
|
20
|
+
onDestroy(() => {
|
|
21
|
+
aborter.abort();
|
|
22
|
+
});
|
|
23
|
+
|
|
19
24
|
let backup: (() => void) | undefined = $state();
|
|
20
25
|
readyUp(
|
|
21
26
|
app,
|
|
22
27
|
intents || [],
|
|
23
28
|
getSyncStrategy,
|
|
29
|
+
aborter.signal,
|
|
24
30
|
(startBackup) =>
|
|
25
31
|
(backup = () => {
|
|
26
32
|
startBackup();
|
package/dist/readyup.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type Intent } from "./utils-transport.js";
|
|
2
2
|
import type { SyncStrategy } from "./storage/utils-storage.js";
|
|
3
|
-
export declare const readyUp: (app: string, intents: Intent[], getSyncStrategy: (path: string) => SyncStrategy, requestBackup: (startBackup: () => void) => void) => void;
|
|
3
|
+
export declare const readyUp: (app: string, intents: Intent[], getSyncStrategy: (path: string) => SyncStrategy, signal: AbortSignal, requestBackup: (startBackup: () => void) => void) => void;
|
package/dist/readyup.js
CHANGED
|
@@ -3,10 +3,13 @@ import {} from "./utils-transport.js";
|
|
|
3
3
|
// import { wrapBackup } from "./storage/wrapbackup.js";
|
|
4
4
|
// import { wrapCloud } from "./storage/wrapcloud.js";
|
|
5
5
|
import { conf, setLoginRecognized } from "./storage.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { pullFromLocalBackup } from "./storage/backuplocally-pull.js";
|
|
7
|
+
import { mountLocalBackupPush } from "./storage/backuplocally-push.js";
|
|
8
|
+
import { createCloudClient } from "./storage/backupcloud-connection.js";
|
|
9
|
+
import { mountCloudPull, pullFromCloud } from "./storage/backupcloud-pull.js";
|
|
10
|
+
import { mountCloudPush } from "./storage/backupcloud-push.js";
|
|
8
11
|
import { switchToHub } from "./utils-hub.js";
|
|
9
|
-
export const readyUp = (app, intents, getSyncStrategy, requestBackup) => {
|
|
12
|
+
export const readyUp = (app, intents, getSyncStrategy, signal, requestBackup) => {
|
|
10
13
|
conf(app);
|
|
11
14
|
let setup = localStorage["monoidentity-x/setup"]
|
|
12
15
|
? JSON.parse(localStorage["monoidentity-x/setup"])
|
|
@@ -34,9 +37,27 @@ export const readyUp = (app, intents, getSyncStrategy, requestBackup) => {
|
|
|
34
37
|
}
|
|
35
38
|
// fire off backup
|
|
36
39
|
if (setup.method == "localStorage") {
|
|
37
|
-
|
|
40
|
+
void pullFromLocalBackup(requestBackup)
|
|
41
|
+
.then((dir) => {
|
|
42
|
+
signal.throwIfAborted();
|
|
43
|
+
if (!dir)
|
|
44
|
+
return;
|
|
45
|
+
mountLocalBackupPush(getSyncStrategy, dir, signal);
|
|
46
|
+
})
|
|
47
|
+
.catch((err) => {
|
|
48
|
+
console.error("[monoidentity local] pull failed", err);
|
|
49
|
+
});
|
|
38
50
|
}
|
|
39
51
|
if (setup.method == "cloud") {
|
|
40
|
-
|
|
52
|
+
const client = createCloudClient(setup);
|
|
53
|
+
void pullFromCloud(getSyncStrategy, setup, client)
|
|
54
|
+
.then(() => {
|
|
55
|
+
signal.throwIfAborted();
|
|
56
|
+
mountCloudPull(getSyncStrategy, setup, client, signal);
|
|
57
|
+
mountCloudPush(getSyncStrategy, setup, client, signal);
|
|
58
|
+
})
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
console.error("[monoidentity cloud] pull failed", err);
|
|
61
|
+
});
|
|
41
62
|
}
|
|
42
63
|
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const isPlainTextCloudObject: (key: string) => boolean;
|
|
2
|
+
export declare const encodeCloudContent: (key: string, value: string) => string | Uint8Array<ArrayBuffer>;
|
|
3
|
+
export declare const decodeCloudContent: (key: string, response: Response) => Promise<string>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const isPlainTextCloudObject = (key) => key.endsWith(".md") || key.endsWith(".devalue");
|
|
2
|
+
export const encodeCloudContent = (key, value) => {
|
|
3
|
+
if (isPlainTextCloudObject(key)) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
const bytes = new Uint8Array(value.length);
|
|
7
|
+
for (let i = 0; i < value.length; i++) {
|
|
8
|
+
bytes[i] = value.charCodeAt(i);
|
|
9
|
+
}
|
|
10
|
+
return bytes;
|
|
11
|
+
};
|
|
12
|
+
export const decodeCloudContent = async (key, response) => {
|
|
13
|
+
if (isPlainTextCloudObject(key)) {
|
|
14
|
+
return response.text();
|
|
15
|
+
}
|
|
16
|
+
const buf = new Uint8Array(await response.arrayBuffer());
|
|
17
|
+
let content = "";
|
|
18
|
+
const chunk = 8192;
|
|
19
|
+
for (let i = 0; i < buf.length; i += chunk) {
|
|
20
|
+
content += String.fromCharCode.apply(null, buf.subarray(i, i + chunk));
|
|
21
|
+
}
|
|
22
|
+
return content;
|
|
23
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AwsClient } from "aws4fetch";
|
|
2
|
+
export const createCloudClient = (bucket) => {
|
|
3
|
+
const awsClient = new AwsClient({
|
|
4
|
+
accessKeyId: bucket.accessKeyId,
|
|
5
|
+
secretAccessKey: bucket.secretAccessKey,
|
|
6
|
+
});
|
|
7
|
+
return (url, options) => awsClient.fetch(url, { ...options, aws: { signQuery: true } });
|
|
8
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Bucket } from "../utils-transport.js";
|
|
2
|
+
import { type SyncStrategy } from "./utils-storage.js";
|
|
3
|
+
export type AwsFetch = (url: string, options?: RequestInit) => Promise<Response>;
|
|
4
|
+
export declare const setCloudCacheEntry: (key: string, etag: string, content: string) => Promise<void>;
|
|
5
|
+
export declare const pullFromCloud: (getSyncStrategy: (path: string) => SyncStrategy, bucket: Bucket, client: AwsFetch) => Promise<void>;
|
|
6
|
+
export declare const mountCloudPull: (getSyncStrategy: (path: string) => SyncStrategy, bucket: Bucket, client: AwsFetch, signal: AbortSignal) => () => void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { storageClient } from "./storageclient.svelte.js";
|
|
2
|
+
import { addSync } from "./utils-sync.js";
|
|
3
|
+
import { shouldPersist } from "./utils-storage.js";
|
|
4
|
+
import { get, set } from "idb-keyval";
|
|
5
|
+
import { store } from "./utils-idb.js";
|
|
6
|
+
import { decodeCloudContent } from "./_backupcloud.js";
|
|
7
|
+
const CLOUD_CACHE_KEY = "cloud-cache";
|
|
8
|
+
let cache;
|
|
9
|
+
const initCache = async () => {
|
|
10
|
+
cache = (await get(CLOUD_CACHE_KEY, store)) || {};
|
|
11
|
+
};
|
|
12
|
+
const getCache = () => {
|
|
13
|
+
if (!cache)
|
|
14
|
+
throw new Error("Cache not initialized");
|
|
15
|
+
return cache;
|
|
16
|
+
};
|
|
17
|
+
const saveCache = async () => {
|
|
18
|
+
await set(CLOUD_CACHE_KEY, getCache(), store);
|
|
19
|
+
};
|
|
20
|
+
export const setCloudCacheEntry = async (key, etag, content) => {
|
|
21
|
+
await initCache();
|
|
22
|
+
getCache()[key] = { etag, content };
|
|
23
|
+
await saveCache();
|
|
24
|
+
};
|
|
25
|
+
const loadFromCloud = async (getSyncStrategy, base, client) => {
|
|
26
|
+
const cacheInit = initCache();
|
|
27
|
+
const listResp = await client(base);
|
|
28
|
+
if (!listResp.ok)
|
|
29
|
+
throw new Error(`List bucket failed: ${listResp.status}`);
|
|
30
|
+
const listXml = await listResp.text();
|
|
31
|
+
const objects = [...listXml.matchAll(/<Key>(.*?)<\/Key>.*?<ETag>(.*?)<\/ETag>/gs)]
|
|
32
|
+
.map((m) => m.slice(1).map((s) => s.replaceAll(""", `"`).replaceAll("'", `'`)))
|
|
33
|
+
.map(([key, etag]) => ({ key, etag: etag.replaceAll(`"`, "") }))
|
|
34
|
+
.filter(({ key }) => getSyncStrategy(key));
|
|
35
|
+
await cacheInit;
|
|
36
|
+
const prevCache = getCache();
|
|
37
|
+
const nextCache = {};
|
|
38
|
+
const model = {};
|
|
39
|
+
await Promise.all(objects.map(async ({ key, etag }) => {
|
|
40
|
+
const cached = prevCache[key];
|
|
41
|
+
if (cached?.etag == etag) {
|
|
42
|
+
model[key] = cached.content;
|
|
43
|
+
nextCache[key] = cached;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
console.debug("[monoidentity cloud] loading", key);
|
|
47
|
+
const r = await client(`${base}/${key}`);
|
|
48
|
+
if (!r.ok)
|
|
49
|
+
throw new Error(`Fetch ${key} failed: ${r.status}`);
|
|
50
|
+
const content = await decodeCloudContent(key, r);
|
|
51
|
+
model[key] = content;
|
|
52
|
+
nextCache[key] = { etag, content };
|
|
53
|
+
}));
|
|
54
|
+
cache = nextCache;
|
|
55
|
+
await saveCache();
|
|
56
|
+
return model;
|
|
57
|
+
};
|
|
58
|
+
const _pullFromCloud = async (getSyncStrategy, bucket, client) => {
|
|
59
|
+
const remote = await loadFromCloud(getSyncStrategy, bucket.base, client);
|
|
60
|
+
const local = storageClient();
|
|
61
|
+
for (const key of Object.keys(local)) {
|
|
62
|
+
if (key in remote)
|
|
63
|
+
continue;
|
|
64
|
+
if (shouldPersist(key))
|
|
65
|
+
continue;
|
|
66
|
+
delete local[key];
|
|
67
|
+
}
|
|
68
|
+
for (const [key, value] of Object.entries(remote)) {
|
|
69
|
+
if (local[key] == value)
|
|
70
|
+
continue;
|
|
71
|
+
local[key] = value;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
export const pullFromCloud = async (getSyncStrategy, bucket, client) => {
|
|
75
|
+
const promise = _pullFromCloud(getSyncStrategy, bucket, client);
|
|
76
|
+
addSync("*", promise);
|
|
77
|
+
await promise;
|
|
78
|
+
};
|
|
79
|
+
export const mountCloudPull = (getSyncStrategy, bucket, client, signal) => {
|
|
80
|
+
signal.throwIfAborted();
|
|
81
|
+
const syncIntervalId = setInterval(() => {
|
|
82
|
+
pullFromCloud(getSyncStrategy, bucket, client).catch((err) => {
|
|
83
|
+
console.error("[monoidentity cloud] pull failed", err);
|
|
84
|
+
});
|
|
85
|
+
}, 15 * 60 * 1000);
|
|
86
|
+
const cleanup = () => {
|
|
87
|
+
clearInterval(syncIntervalId);
|
|
88
|
+
signal.removeEventListener("abort", cleanup);
|
|
89
|
+
};
|
|
90
|
+
signal.addEventListener("abort", cleanup, { once: true });
|
|
91
|
+
return cleanup;
|
|
92
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Bucket } from "../utils-transport.js";
|
|
2
|
+
import { type SyncStrategy } from "./utils-storage.js";
|
|
3
|
+
import { type AwsFetch } from "./backupcloud-pull.js";
|
|
4
|
+
export declare const mountCloudPush: (getSyncStrategy: (path: string) => SyncStrategy, bucket: Bucket, client: AwsFetch, signal: AbortSignal) => () => void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { STORAGE_EVENT } from "./storageclient.svelte.js";
|
|
2
|
+
import { addSync, scheduleSync } from "./utils-sync.js";
|
|
3
|
+
import { shouldPersist } from "./utils-storage.js";
|
|
4
|
+
import { setCloudCacheEntry } from "./backupcloud-pull.js";
|
|
5
|
+
import { encodeCloudContent } from "./_backupcloud.js";
|
|
6
|
+
const write = async (key, value, bucket, client) => {
|
|
7
|
+
console.debug("[monoidentity cloud] saving", key);
|
|
8
|
+
const url = `${bucket.base}/${key}`;
|
|
9
|
+
if (value != undefined) {
|
|
10
|
+
const r = await client(url, {
|
|
11
|
+
method: "PUT",
|
|
12
|
+
headers: { "content-type": "application/octet-stream" },
|
|
13
|
+
body: encodeCloudContent(key, value),
|
|
14
|
+
});
|
|
15
|
+
if (!r.ok)
|
|
16
|
+
throw new Error(`PUT ${key} failed: ${r.status}`);
|
|
17
|
+
const etag = r.headers.get("etag")?.replaceAll('"', "");
|
|
18
|
+
if (etag) {
|
|
19
|
+
await setCloudCacheEntry(key, etag, value);
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const r = await client(url, { method: "DELETE" });
|
|
24
|
+
if (!r.ok && r.status != 404)
|
|
25
|
+
throw new Error(`DELETE ${key} failed: ${r.status}`);
|
|
26
|
+
};
|
|
27
|
+
export const mountCloudPush = (getSyncStrategy, bucket, client, signal) => {
|
|
28
|
+
signal.throwIfAborted();
|
|
29
|
+
const writeWrapped = async (key, value) => write(key, value, bucket, client).catch((err) => {
|
|
30
|
+
console.error("[monoidentity cloud] save failed", key, err);
|
|
31
|
+
});
|
|
32
|
+
const listener = (event) => {
|
|
33
|
+
const fullKey = event.detail.key;
|
|
34
|
+
if (!fullKey.startsWith("monoidentity/"))
|
|
35
|
+
return;
|
|
36
|
+
const key = fullKey.slice("monoidentity/".length);
|
|
37
|
+
const strategy = getSyncStrategy(key);
|
|
38
|
+
if (!strategy) {
|
|
39
|
+
if (!shouldPersist(key))
|
|
40
|
+
console.warn("[monoidentity cloud]", key, "isn't marked to be synced");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (strategy.mode == "immediate") {
|
|
44
|
+
addSync(fullKey, writeWrapped(key, event.detail.value));
|
|
45
|
+
}
|
|
46
|
+
else if (strategy.mode == "debounced") {
|
|
47
|
+
scheduleSync(fullKey, () => writeWrapped(key, localStorage[fullKey]), strategy.debounceMs);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
addEventListener(STORAGE_EVENT, listener);
|
|
51
|
+
const cleanup = () => {
|
|
52
|
+
removeEventListener(STORAGE_EVENT, listener);
|
|
53
|
+
signal.removeEventListener("abort", cleanup);
|
|
54
|
+
};
|
|
55
|
+
signal.addEventListener("abort", cleanup, { once: true });
|
|
56
|
+
return cleanup;
|
|
57
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const pullFromLocalBackup: (requestBackup: (startBackup: () => void) => void) => Promise<FileSystemDirectoryHandle | undefined>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { get, set } from "idb-keyval";
|
|
2
|
+
import { canBackup } from "../utils-localstorage.js";
|
|
3
|
+
import { storageClient } from "./storageclient.svelte.js";
|
|
4
|
+
import { store } from "./utils-idb.js";
|
|
5
|
+
import { addSync } from "./utils-sync.js";
|
|
6
|
+
const HANDLE_KEY = "backup-handle";
|
|
7
|
+
const HANDLE_DISABLED_KEY = "backup-handle-disabled";
|
|
8
|
+
const restoreFromDir = async (dir) => {
|
|
9
|
+
const backup = {};
|
|
10
|
+
const traverse = async (d, path) => {
|
|
11
|
+
for await (const entry of d.values()) {
|
|
12
|
+
if (entry.kind == "file") {
|
|
13
|
+
const file = await entry.getFile();
|
|
14
|
+
const text = await file.text();
|
|
15
|
+
backup[`${path}${entry.name}`] = text;
|
|
16
|
+
}
|
|
17
|
+
else if (entry.kind == "directory") {
|
|
18
|
+
await traverse(entry, `${path}${entry.name}/`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
await traverse(dir, "");
|
|
23
|
+
if (!Object.keys(backup).length)
|
|
24
|
+
return;
|
|
25
|
+
const client = storageClient();
|
|
26
|
+
for (const key in backup) {
|
|
27
|
+
console.debug("[monoidentity local] loading", key);
|
|
28
|
+
client[key] = backup[key];
|
|
29
|
+
}
|
|
30
|
+
location.reload();
|
|
31
|
+
};
|
|
32
|
+
export const pullFromLocalBackup = async (requestBackup) => {
|
|
33
|
+
if (!canBackup)
|
|
34
|
+
return;
|
|
35
|
+
const dir = await get(HANDLE_KEY, store);
|
|
36
|
+
if (dir) {
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
const disabled = await get(HANDLE_DISABLED_KEY, store);
|
|
40
|
+
if (disabled) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await set(HANDLE_DISABLED_KEY, true);
|
|
44
|
+
return new Promise((resolve, reject) => requestBackup(async () => {
|
|
45
|
+
try {
|
|
46
|
+
const dir = await showDirectoryPicker({ mode: "readwrite" });
|
|
47
|
+
await set(HANDLE_KEY, dir, store);
|
|
48
|
+
await set(HANDLE_DISABLED_KEY, false);
|
|
49
|
+
const restorePromise = restoreFromDir(dir);
|
|
50
|
+
addSync("*", restorePromise);
|
|
51
|
+
await restorePromise;
|
|
52
|
+
resolve(dir);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
reject(error);
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { STORAGE_EVENT } from "./storageclient.svelte.js";
|
|
2
|
+
import { shouldPersist } from "./utils-storage.js";
|
|
3
|
+
import { addSync } from "./utils-sync.js";
|
|
4
|
+
const saveToDir = (getSyncStrategy, dir) => {
|
|
5
|
+
let dirCache = {};
|
|
6
|
+
const getDirCached = async (route) => {
|
|
7
|
+
let key = "";
|
|
8
|
+
let parent = dir;
|
|
9
|
+
for (const path of route) {
|
|
10
|
+
key += "/";
|
|
11
|
+
key += path;
|
|
12
|
+
if (!dirCache[key]) {
|
|
13
|
+
dirCache[key] = await parent.getDirectoryHandle(path, { create: true });
|
|
14
|
+
}
|
|
15
|
+
parent = dirCache[key];
|
|
16
|
+
}
|
|
17
|
+
return parent;
|
|
18
|
+
};
|
|
19
|
+
const writeFile = async (key, value) => {
|
|
20
|
+
const pathParts = key.split("/");
|
|
21
|
+
const name = pathParts.at(-1);
|
|
22
|
+
console.debug("[monoidentity local] saving", name);
|
|
23
|
+
const parent = await getDirCached(pathParts.slice(0, -1));
|
|
24
|
+
if (value != undefined) {
|
|
25
|
+
const file = await parent.getFileHandle(name, { create: true });
|
|
26
|
+
const writable = await file.createWritable();
|
|
27
|
+
await writable.write(value);
|
|
28
|
+
await writable.close();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
await parent.removeEntry(name);
|
|
32
|
+
};
|
|
33
|
+
const listener = (event) => {
|
|
34
|
+
const fullKey = event.detail.key;
|
|
35
|
+
if (!fullKey.startsWith("monoidentity/"))
|
|
36
|
+
return;
|
|
37
|
+
const key = fullKey.slice("monoidentity/".length);
|
|
38
|
+
const strategy = getSyncStrategy(key);
|
|
39
|
+
if (!strategy) {
|
|
40
|
+
if (!shouldPersist(key))
|
|
41
|
+
console.warn("[monoidentity local]", key, "isn't marked to be backed up or saved");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
addSync(key, writeFile(key, event.detail.value));
|
|
45
|
+
};
|
|
46
|
+
addEventListener(STORAGE_EVENT, listener);
|
|
47
|
+
return () => {
|
|
48
|
+
removeEventListener(STORAGE_EVENT, listener);
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export const mountLocalBackupPush = (getSyncStrategy, dir, signal) => {
|
|
52
|
+
signal.throwIfAborted();
|
|
53
|
+
const unmount = saveToDir(getSyncStrategy, dir);
|
|
54
|
+
const cleanup = () => {
|
|
55
|
+
unmount();
|
|
56
|
+
signal.removeEventListener("abort", onAbort);
|
|
57
|
+
};
|
|
58
|
+
const onAbort = () => {
|
|
59
|
+
cleanup();
|
|
60
|
+
};
|
|
61
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
62
|
+
return cleanup;
|
|
63
|
+
};
|
|
@@ -11,7 +11,9 @@ const runScheduledSync = async (key) => {
|
|
|
11
11
|
if (!scheduled)
|
|
12
12
|
return;
|
|
13
13
|
delete scheduledSyncs[key];
|
|
14
|
-
|
|
14
|
+
const promise = scheduled.fn();
|
|
15
|
+
addSync(key, promise);
|
|
16
|
+
await promise;
|
|
15
17
|
};
|
|
16
18
|
const scheduleInterval = setInterval(() => {
|
|
17
19
|
const now = Date.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monoidentity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"repository": "KTibow/monoidentity",
|
|
6
6
|
"author": {
|
|
@@ -31,29 +31,29 @@
|
|
|
31
31
|
"svelte": "^5.0.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tsndr/cloudflare-worker-jwt": "^3.2.
|
|
34
|
+
"@tsndr/cloudflare-worker-jwt": "^3.2.1",
|
|
35
35
|
"aws4fetch": "^1.0.20",
|
|
36
|
-
"devalue": "^5.
|
|
36
|
+
"devalue": "^5.6.2",
|
|
37
37
|
"fast-studentvue": "^2.1.1",
|
|
38
38
|
"idb-keyval": "^6.2.2",
|
|
39
|
-
"valibot": "^1.
|
|
39
|
+
"valibot": "^1.2.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
43
|
-
"@sveltejs/kit": "^2.
|
|
44
|
-
"@sveltejs/package": "^2.5.
|
|
45
|
-
"@sveltejs/vite-plugin-svelte": "^6.2.
|
|
43
|
+
"@sveltejs/kit": "^2.51.0",
|
|
44
|
+
"@sveltejs/package": "^2.5.7",
|
|
45
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
46
46
|
"@types/wicg-file-system-access": "^2023.10.7",
|
|
47
|
-
"knip": "^5.
|
|
48
|
-
"monoserve": "^3.2.
|
|
49
|
-
"publint": "^0.3.
|
|
50
|
-
"rolldown": "
|
|
47
|
+
"knip": "^5.83.1",
|
|
48
|
+
"monoserve": "^3.2.3",
|
|
49
|
+
"publint": "^0.3.17",
|
|
50
|
+
"rolldown": "1.0.0-rc.4",
|
|
51
51
|
"school-districts": "^5.0.1",
|
|
52
|
-
"svelte": "^5.
|
|
53
|
-
"svelte-check": "^4.
|
|
52
|
+
"svelte": "^5.51.0",
|
|
53
|
+
"svelte-check": "^4.4.0",
|
|
54
54
|
"tinyglobby": "^0.2.15",
|
|
55
55
|
"typescript": "^5.9.3",
|
|
56
|
-
"vite": "^7.
|
|
56
|
+
"vite": "^7.3.1"
|
|
57
57
|
},
|
|
58
58
|
"keywords": [
|
|
59
59
|
"svelte"
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { AwsClient } from "aws4fetch";
|
|
2
|
-
import { storageClient, STORAGE_EVENT } from "./storageclient.svelte.js";
|
|
3
|
-
import { addSync, scheduleSync } from "./utils-sync.js";
|
|
4
|
-
import { shouldPersist } from "./utils-storage.js";
|
|
5
|
-
import { get, set } from "idb-keyval";
|
|
6
|
-
import { store } from "./utils-idb.js";
|
|
7
|
-
const CLOUD_CACHE_KEY = "cloud-cache";
|
|
8
|
-
let unmount;
|
|
9
|
-
let cache;
|
|
10
|
-
const initCache = async () => {
|
|
11
|
-
cache = (await get(CLOUD_CACHE_KEY, store)) || {};
|
|
12
|
-
};
|
|
13
|
-
const getCache = () => {
|
|
14
|
-
if (!cache)
|
|
15
|
-
throw new Error("Cache not initialized");
|
|
16
|
-
return cache;
|
|
17
|
-
};
|
|
18
|
-
const saveCache = async () => {
|
|
19
|
-
if (!cache)
|
|
20
|
-
throw new Error("Cache not initialized");
|
|
21
|
-
await set(CLOUD_CACHE_KEY, cache, store);
|
|
22
|
-
};
|
|
23
|
-
const loadFromCloud = async (getSyncStrategy, base, client) => {
|
|
24
|
-
const cachePromise = initCache();
|
|
25
|
-
const listResp = await client(base);
|
|
26
|
-
if (!listResp.ok)
|
|
27
|
-
throw new Error(`List bucket failed: ${listResp.status}`);
|
|
28
|
-
const listXml = await listResp.text();
|
|
29
|
-
const objects = [...listXml.matchAll(/<Key>(.*?)<\/Key>.*?<ETag>(.*?)<\/ETag>/gs)]
|
|
30
|
-
.map((m) => m.slice(1).map((s) => s.replaceAll(""", `"`).replaceAll("'", `'`)))
|
|
31
|
-
.map(([key, etag]) => ({ key, etag: etag.replaceAll(`"`, "") }))
|
|
32
|
-
.filter(({ key }) => getSyncStrategy(key));
|
|
33
|
-
await cachePromise;
|
|
34
|
-
const prevCache = getCache();
|
|
35
|
-
const nextCache = {};
|
|
36
|
-
const model = {};
|
|
37
|
-
await Promise.all(objects.map(async ({ key, etag }) => {
|
|
38
|
-
const cached = prevCache[key];
|
|
39
|
-
if (cached?.etag == etag) {
|
|
40
|
-
model[key] = cached.content;
|
|
41
|
-
nextCache[key] = cached;
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
console.debug("[monoidentity cloud] loading", key);
|
|
45
|
-
const r = await client(`${base}/${key}`);
|
|
46
|
-
if (!r.ok)
|
|
47
|
-
throw new Error(`Fetch ${key} failed: ${r.status}`);
|
|
48
|
-
let content;
|
|
49
|
-
if (key.endsWith(".md") || key.endsWith(".devalue")) {
|
|
50
|
-
content = await r.text();
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
const buf = new Uint8Array(await r.arrayBuffer());
|
|
54
|
-
content = "";
|
|
55
|
-
const chunk = 8192;
|
|
56
|
-
for (let i = 0; i < buf.length; i += chunk) {
|
|
57
|
-
content += String.fromCharCode.apply(null, buf.subarray(i, i + chunk));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
model[key] = content;
|
|
61
|
-
nextCache[key] = { etag, content };
|
|
62
|
-
}));
|
|
63
|
-
cache = nextCache;
|
|
64
|
-
saveCache();
|
|
65
|
-
return model;
|
|
66
|
-
};
|
|
67
|
-
const _syncFromCloud = async (getSyncStrategy, bucket, client) => {
|
|
68
|
-
const remote = await loadFromCloud(getSyncStrategy, bucket.base, client);
|
|
69
|
-
const local = storageClient();
|
|
70
|
-
for (const key of Object.keys(local)) {
|
|
71
|
-
if (key in remote)
|
|
72
|
-
continue;
|
|
73
|
-
if (shouldPersist(key))
|
|
74
|
-
continue;
|
|
75
|
-
delete local[key];
|
|
76
|
-
}
|
|
77
|
-
for (const [key, value] of Object.entries(remote)) {
|
|
78
|
-
if (local[key] == value)
|
|
79
|
-
continue;
|
|
80
|
-
local[key] = value;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
const syncFromCloud = async (getSyncStrategy, bucket, client) => {
|
|
84
|
-
const promise = _syncFromCloud(getSyncStrategy, bucket, client);
|
|
85
|
-
addSync("*", promise);
|
|
86
|
-
await promise;
|
|
87
|
-
};
|
|
88
|
-
export const backupCloud = async (getSyncStrategy, bucket) => {
|
|
89
|
-
unmount?.();
|
|
90
|
-
const awsClient = new AwsClient({
|
|
91
|
-
accessKeyId: bucket.accessKeyId,
|
|
92
|
-
secretAccessKey: bucket.secretAccessKey,
|
|
93
|
-
});
|
|
94
|
-
const client = async (url, options) => {
|
|
95
|
-
return awsClient.fetch(url, { ...options, aws: { signQuery: true } });
|
|
96
|
-
};
|
|
97
|
-
await syncFromCloud(getSyncStrategy, bucket, client);
|
|
98
|
-
const syncIntervalId = setInterval(() => syncFromCloud(getSyncStrategy, bucket, client), 15 * 60 * 1000);
|
|
99
|
-
// Continuous sync: mirror local changes to cloud
|
|
100
|
-
const write = async (key, value) => {
|
|
101
|
-
console.debug("[monoidentity cloud] saving", key);
|
|
102
|
-
const url = `${bucket.base}/${key}`;
|
|
103
|
-
if (value != undefined) {
|
|
104
|
-
// PUT content (unconditional to start; you can add If-Match/If-None-Match for safety)
|
|
105
|
-
const r = await client(url, {
|
|
106
|
-
method: "PUT",
|
|
107
|
-
headers: { "content-type": "application/octet-stream" },
|
|
108
|
-
body: value,
|
|
109
|
-
});
|
|
110
|
-
if (!r.ok)
|
|
111
|
-
throw new Error(`PUT ${key} failed: ${r.status}`);
|
|
112
|
-
// Update cache
|
|
113
|
-
const etag = r.headers.get("etag")?.replaceAll('"', "");
|
|
114
|
-
if (etag) {
|
|
115
|
-
getCache()[key] = { etag, content: value };
|
|
116
|
-
saveCache();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
// DELETE key
|
|
121
|
-
const r = await client(url, { method: "DELETE" });
|
|
122
|
-
if (!r.ok && r.status != 404)
|
|
123
|
-
throw new Error(`DELETE ${key} failed: ${r.status}`);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
const writeWrapped = async (key, value) => write(key, value).catch((err) => {
|
|
127
|
-
console.error("[monoidentity cloud] save failed", key, err);
|
|
128
|
-
});
|
|
129
|
-
const listener = (event) => {
|
|
130
|
-
const fullKey = event.detail.key;
|
|
131
|
-
if (!fullKey.startsWith("monoidentity/"))
|
|
132
|
-
return;
|
|
133
|
-
const key = fullKey.slice("monoidentity/".length);
|
|
134
|
-
const strategy = getSyncStrategy(key);
|
|
135
|
-
if (!strategy) {
|
|
136
|
-
if (!shouldPersist(key))
|
|
137
|
-
console.warn("[monoidentity cloud]", key, "isn't marked to be backed up or saved");
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (strategy.mode == "immediate") {
|
|
141
|
-
addSync(fullKey, writeWrapped(key, event.detail.value));
|
|
142
|
-
}
|
|
143
|
-
else if (strategy.mode == "debounced") {
|
|
144
|
-
scheduleSync(fullKey, () => writeWrapped(key, localStorage[fullKey]), strategy.debounceMs);
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
addEventListener(STORAGE_EVENT, listener);
|
|
148
|
-
unmount = () => {
|
|
149
|
-
clearInterval(syncIntervalId);
|
|
150
|
-
removeEventListener(STORAGE_EVENT, listener);
|
|
151
|
-
};
|
|
152
|
-
};
|
|
153
|
-
if (import.meta.hot) {
|
|
154
|
-
import.meta.hot.dispose(() => {
|
|
155
|
-
unmount?.();
|
|
156
|
-
});
|
|
157
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { get, set } from "idb-keyval";
|
|
2
|
-
import { STORAGE_EVENT, storageClient } from "./storageclient.svelte.js";
|
|
3
|
-
import { canBackup } from "../utils-localstorage.js";
|
|
4
|
-
import { shouldPersist } from "./utils-storage.js";
|
|
5
|
-
import { store } from "./utils-idb.js";
|
|
6
|
-
import { addSync } from "./utils-sync.js";
|
|
7
|
-
const TOGGLE_KEY = "monoidentity-x/local-backup";
|
|
8
|
-
const HANDLE_KEY = "backup-handle";
|
|
9
|
-
let unmount;
|
|
10
|
-
const saveToDir = (getSyncStrategy, dir) => {
|
|
11
|
-
let dirCache = {};
|
|
12
|
-
const getDirCached = async (route) => {
|
|
13
|
-
let key = "";
|
|
14
|
-
let parent = dir;
|
|
15
|
-
for (const path of route) {
|
|
16
|
-
key += "/";
|
|
17
|
-
key += path;
|
|
18
|
-
if (!dirCache[key]) {
|
|
19
|
-
dirCache[key] = await parent.getDirectoryHandle(path, { create: true });
|
|
20
|
-
}
|
|
21
|
-
parent = dirCache[key];
|
|
22
|
-
}
|
|
23
|
-
return parent;
|
|
24
|
-
};
|
|
25
|
-
const writeFile = async (key, value) => {
|
|
26
|
-
const pathParts = key.split("/");
|
|
27
|
-
const name = pathParts.at(-1);
|
|
28
|
-
console.debug("[monoidentity backup] saving", name);
|
|
29
|
-
const parent = await getDirCached(pathParts.slice(0, -1));
|
|
30
|
-
if (value != undefined) {
|
|
31
|
-
const file = await parent.getFileHandle(name, { create: true });
|
|
32
|
-
const writable = await file.createWritable();
|
|
33
|
-
await writable.write(value);
|
|
34
|
-
await writable.close();
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
await parent.removeEntry(name);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
const listener = (event) => {
|
|
41
|
-
const fullKey = event.detail.key;
|
|
42
|
-
if (!fullKey.startsWith("monoidentity/"))
|
|
43
|
-
return;
|
|
44
|
-
const key = fullKey.slice("monoidentity/".length);
|
|
45
|
-
const strategy = getSyncStrategy(key);
|
|
46
|
-
if (!strategy) {
|
|
47
|
-
if (!shouldPersist(key))
|
|
48
|
-
console.warn("[monoidentity backup]", key, "isn't marked to be backed up or saved");
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
addSync(key, writeFile(key, event.detail.value));
|
|
52
|
-
};
|
|
53
|
-
addEventListener(STORAGE_EVENT, listener);
|
|
54
|
-
return () => {
|
|
55
|
-
removeEventListener(STORAGE_EVENT, listener);
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
export const backupLocally = async (getSyncStrategy, requestBackup) => {
|
|
59
|
-
if (!canBackup)
|
|
60
|
-
return;
|
|
61
|
-
if (localStorage[TOGGLE_KEY] == "off")
|
|
62
|
-
return;
|
|
63
|
-
unmount?.();
|
|
64
|
-
if (localStorage[TOGGLE_KEY] == "on") {
|
|
65
|
-
const dir = await get(HANDLE_KEY, store);
|
|
66
|
-
if (!dir)
|
|
67
|
-
throw new Error("No backup handle found");
|
|
68
|
-
unmount = saveToDir(getSyncStrategy, dir);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
localStorage[TOGGLE_KEY] = "off";
|
|
72
|
-
requestBackup(async () => {
|
|
73
|
-
const dir = await showDirectoryPicker({ mode: "readwrite" });
|
|
74
|
-
await set(HANDLE_KEY, dir, store);
|
|
75
|
-
localStorage[TOGGLE_KEY] = "on";
|
|
76
|
-
// Restore from backup
|
|
77
|
-
const restorePromise = (async () => {
|
|
78
|
-
const backup = {};
|
|
79
|
-
const traverse = async (d, path) => {
|
|
80
|
-
for await (const entry of d.values()) {
|
|
81
|
-
if (entry.kind == "file") {
|
|
82
|
-
const file = await entry.getFile();
|
|
83
|
-
const text = await file.text();
|
|
84
|
-
backup[`${path}${entry.name}`] = text;
|
|
85
|
-
}
|
|
86
|
-
else if (entry.kind == "directory") {
|
|
87
|
-
await traverse(entry, `${path}${entry.name}/`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
await traverse(dir, "");
|
|
92
|
-
if (Object.keys(backup).length) {
|
|
93
|
-
const client = storageClient();
|
|
94
|
-
for (const key in backup) {
|
|
95
|
-
console.debug("[monoidentity backup] loading", key);
|
|
96
|
-
client[key] = backup[key];
|
|
97
|
-
}
|
|
98
|
-
location.reload();
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
})();
|
|
102
|
-
addSync("*", restorePromise);
|
|
103
|
-
await restorePromise;
|
|
104
|
-
unmount = saveToDir(getSyncStrategy, dir);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
if (import.meta.hot) {
|
|
109
|
-
import.meta.hot.dispose(() => {
|
|
110
|
-
unmount?.();
|
|
111
|
-
});
|
|
112
|
-
}
|