monoidentity 0.20.0 → 0.21.0

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.
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { Snippet } from "svelte";
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();
@@ -1,4 +1,4 @@
1
- import type { Snippet } from "svelte";
1
+ import { type Snippet } from "svelte";
2
2
  import type { Intent } from "./utils-transport.js";
3
3
  import type { SyncStrategy } from "./storage/utils-storage.js";
4
4
  type $$ComponentProps = {
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 { backupLocally } from "./storage/backuplocally.js";
7
- import { backupCloud } from "./storage/backupcloud.js";
6
+ import { pullFromLocalBackup } from "./storage/backuplocally-pull.js";
7
+ import { mountLocalBackupPush } from "./storage/backuplocally-push.js";
8
+ import { createCloudClient } from "./storage/backupcloud-shared.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
- backupLocally(getSyncStrategy, requestBackup);
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
- backupCloud(getSyncStrategy, setup);
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,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,102 @@
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
+ const CLOUD_CACHE_KEY = "cloud-cache";
7
+ let cache;
8
+ const initCache = async () => {
9
+ cache = (await get(CLOUD_CACHE_KEY, store)) || {};
10
+ };
11
+ const getCache = () => {
12
+ if (!cache)
13
+ throw new Error("Cache not initialized");
14
+ return cache;
15
+ };
16
+ const saveCache = async () => {
17
+ await set(CLOUD_CACHE_KEY, getCache(), store);
18
+ };
19
+ export const setCloudCacheEntry = async (key, etag, content) => {
20
+ await initCache();
21
+ getCache()[key] = { etag, content };
22
+ await saveCache();
23
+ };
24
+ const loadFromCloud = async (getSyncStrategy, base, client) => {
25
+ const cacheInit = initCache();
26
+ const listResp = await client(base);
27
+ if (!listResp.ok)
28
+ throw new Error(`List bucket failed: ${listResp.status}`);
29
+ const listXml = await listResp.text();
30
+ const objects = [...listXml.matchAll(/<Key>(.*?)<\/Key>.*?<ETag>(.*?)<\/ETag>/gs)]
31
+ .map((m) => m.slice(1).map((s) => s.replaceAll("&quot;", `"`).replaceAll("&apos;", `'`)))
32
+ .map(([key, etag]) => ({ key, etag: etag.replaceAll(`"`, "") }))
33
+ .filter(({ key }) => getSyncStrategy(key));
34
+ await cacheInit;
35
+ const prevCache = getCache();
36
+ const nextCache = {};
37
+ const model = {};
38
+ await Promise.all(objects.map(async ({ key, etag }) => {
39
+ const cached = prevCache[key];
40
+ if (cached?.etag == etag) {
41
+ model[key] = cached.content;
42
+ nextCache[key] = cached;
43
+ return;
44
+ }
45
+ console.debug("[monoidentity cloud] loading", key);
46
+ const r = await client(`${base}/${key}`);
47
+ if (!r.ok)
48
+ throw new Error(`Fetch ${key} failed: ${r.status}`);
49
+ let content;
50
+ if (key.endsWith(".md") || key.endsWith(".devalue")) {
51
+ content = await r.text();
52
+ }
53
+ else {
54
+ const buf = new Uint8Array(await r.arrayBuffer());
55
+ content = "";
56
+ const chunk = 8192;
57
+ for (let i = 0; i < buf.length; i += chunk) {
58
+ content += String.fromCharCode.apply(null, buf.subarray(i, i + chunk));
59
+ }
60
+ }
61
+ model[key] = content;
62
+ nextCache[key] = { etag, content };
63
+ }));
64
+ cache = nextCache;
65
+ await saveCache();
66
+ return model;
67
+ };
68
+ const _pullFromCloud = async (getSyncStrategy, bucket, client) => {
69
+ const remote = await loadFromCloud(getSyncStrategy, bucket.base, client);
70
+ const local = storageClient();
71
+ for (const key of Object.keys(local)) {
72
+ if (key in remote)
73
+ continue;
74
+ if (shouldPersist(key))
75
+ continue;
76
+ delete local[key];
77
+ }
78
+ for (const [key, value] of Object.entries(remote)) {
79
+ if (local[key] == value)
80
+ continue;
81
+ local[key] = value;
82
+ }
83
+ };
84
+ export const pullFromCloud = async (getSyncStrategy, bucket, client) => {
85
+ const promise = _pullFromCloud(getSyncStrategy, bucket, client);
86
+ addSync("*", promise);
87
+ await promise;
88
+ };
89
+ export const mountCloudPull = (getSyncStrategy, bucket, client, signal) => {
90
+ signal.throwIfAborted();
91
+ const syncIntervalId = setInterval(() => {
92
+ pullFromCloud(getSyncStrategy, bucket, client).catch((err) => {
93
+ console.error("[monoidentity cloud] pull failed", err);
94
+ });
95
+ }, 15 * 60 * 1000);
96
+ const cleanup = () => {
97
+ clearInterval(syncIntervalId);
98
+ signal.removeEventListener("abort", cleanup);
99
+ };
100
+ signal.addEventListener("abort", cleanup, { once: true });
101
+ return cleanup;
102
+ };
@@ -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,56 @@
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
+ const write = async (key, value, bucket, client) => {
6
+ console.debug("[monoidentity cloud] saving", key);
7
+ const url = `${bucket.base}/${key}`;
8
+ if (value != undefined) {
9
+ const r = await client(url, {
10
+ method: "PUT",
11
+ headers: { "content-type": "application/octet-stream" },
12
+ body: value,
13
+ });
14
+ if (!r.ok)
15
+ throw new Error(`PUT ${key} failed: ${r.status}`);
16
+ const etag = r.headers.get("etag")?.replaceAll('"', "");
17
+ if (etag) {
18
+ await setCloudCacheEntry(key, etag, value);
19
+ }
20
+ return;
21
+ }
22
+ const r = await client(url, { method: "DELETE" });
23
+ if (!r.ok && r.status != 404)
24
+ throw new Error(`DELETE ${key} failed: ${r.status}`);
25
+ };
26
+ export const mountCloudPush = (getSyncStrategy, bucket, client, signal) => {
27
+ signal.throwIfAborted();
28
+ const writeWrapped = async (key, value) => write(key, value, bucket, client).catch((err) => {
29
+ console.error("[monoidentity cloud] save failed", key, err);
30
+ });
31
+ const listener = (event) => {
32
+ const fullKey = event.detail.key;
33
+ if (!fullKey.startsWith("monoidentity/"))
34
+ return;
35
+ const key = fullKey.slice("monoidentity/".length);
36
+ const strategy = getSyncStrategy(key);
37
+ if (!strategy) {
38
+ if (!shouldPersist(key))
39
+ console.warn("[monoidentity cloud]", key, "isn't marked to be synced");
40
+ return;
41
+ }
42
+ if (strategy.mode == "immediate") {
43
+ addSync(fullKey, writeWrapped(key, event.detail.value));
44
+ }
45
+ else if (strategy.mode == "debounced") {
46
+ scheduleSync(fullKey, () => writeWrapped(key, localStorage[fullKey]), strategy.debounceMs);
47
+ }
48
+ };
49
+ addEventListener(STORAGE_EVENT, listener);
50
+ const cleanup = () => {
51
+ removeEventListener(STORAGE_EVENT, listener);
52
+ signal.removeEventListener("abort", cleanup);
53
+ };
54
+ signal.addEventListener("abort", cleanup, { once: true });
55
+ return cleanup;
56
+ };
@@ -0,0 +1,3 @@
1
+ import type { Bucket } from "../utils-transport.js";
2
+ import type { AwsFetch } from "./backupcloud-pull.js";
3
+ export declare const createCloudClient: (bucket: Bucket) => AwsFetch;
@@ -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 @@
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,2 @@
1
+ import { type SyncStrategy } from "./utils-storage.js";
2
+ export declare const mountLocalBackupPush: (getSyncStrategy: (path: string) => SyncStrategy, dir: FileSystemDirectoryHandle, signal: AbortSignal) => () => void;
@@ -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
- addSync(key, scheduled.fn());
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.20.0",
3
+ "version": "0.21.0",
4
4
  "license": "ISC",
5
5
  "repository": "KTibow/monoidentity",
6
6
  "author": {
@@ -1,3 +0,0 @@
1
- import type { Bucket } from "../utils-transport.js";
2
- import { type SyncStrategy } from "./utils-storage.js";
3
- export declare const backupCloud: (getSyncStrategy: (path: string) => SyncStrategy, bucket: Bucket) => Promise<void>;
@@ -1,154 +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.fetch(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("&quot;", `"`).replaceAll("&apos;", `'`)))
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.fetch(`${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 client = new AwsClient({
91
- accessKeyId: bucket.accessKeyId,
92
- secretAccessKey: bucket.secretAccessKey,
93
- });
94
- await syncFromCloud(getSyncStrategy, bucket, client);
95
- const syncIntervalId = setInterval(() => syncFromCloud(getSyncStrategy, bucket, client), 15 * 60 * 1000);
96
- // Continuous sync: mirror local changes to cloud
97
- const write = async (key, value) => {
98
- console.debug("[monoidentity cloud] saving", key);
99
- const url = `${bucket.base}/${key}`;
100
- if (value != undefined) {
101
- // PUT content (unconditional to start; you can add If-Match/If-None-Match for safety)
102
- const r = await client.fetch(url, {
103
- method: "PUT",
104
- headers: { "content-type": "application/octet-stream" },
105
- body: value,
106
- });
107
- if (!r.ok)
108
- throw new Error(`PUT ${key} failed: ${r.status}`);
109
- // Update cache
110
- const etag = r.headers.get("etag")?.replaceAll('"', "");
111
- if (etag) {
112
- getCache()[key] = { etag, content: value };
113
- saveCache();
114
- }
115
- }
116
- else {
117
- // DELETE key
118
- const r = await client.fetch(url, { method: "DELETE" });
119
- if (!r.ok && r.status != 404)
120
- throw new Error(`DELETE ${key} failed: ${r.status}`);
121
- }
122
- };
123
- const writeWrapped = async (key, value) => write(key, value).catch((err) => {
124
- console.error("[monoidentity cloud] save failed", key, err);
125
- });
126
- const listener = (event) => {
127
- const fullKey = event.detail.key;
128
- if (!fullKey.startsWith("monoidentity/"))
129
- return;
130
- const key = fullKey.slice("monoidentity/".length);
131
- const strategy = getSyncStrategy(key);
132
- if (!strategy) {
133
- if (!shouldPersist(key))
134
- console.warn("[monoidentity cloud]", key, "isn't marked to be backed up or saved");
135
- return;
136
- }
137
- if (strategy.mode == "immediate") {
138
- addSync(fullKey, writeWrapped(key, event.detail.value));
139
- }
140
- else if (strategy.mode == "debounced") {
141
- scheduleSync(fullKey, () => writeWrapped(key, localStorage[fullKey]), strategy.debounceMs);
142
- }
143
- };
144
- addEventListener(STORAGE_EVENT, listener);
145
- unmount = () => {
146
- clearInterval(syncIntervalId);
147
- removeEventListener(STORAGE_EVENT, listener);
148
- };
149
- };
150
- if (import.meta.hot) {
151
- import.meta.hot.dispose(() => {
152
- unmount?.();
153
- });
154
- }
@@ -1,2 +0,0 @@
1
- import { type SyncStrategy } from "./utils-storage.js";
2
- export declare const backupLocally: (getSyncStrategy: (path: string) => SyncStrategy, requestBackup: (startBackup: () => void) => void) => Promise<void>;
@@ -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
- }