monoidentity 0.16.1 → 0.17.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.
package/dist/+client.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from "./+common.js";
2
2
  export { getLoginRecognized, relog, getVerification, getStorage, getScopedFS, completeSync, } from "./storage.js";
3
+ export type { SyncStrategy } from "./storage/utils-storage.js";
3
4
  export { retrieveVerification } from "./verification-client.js";
4
5
  export { attest as rawAttest } from "./verification/attest.js";
5
6
  export { trackReady } from "./trackready.js";
@@ -2,16 +2,17 @@
2
2
  import type { Snippet } from "svelte";
3
3
  import { trackReady } from "./trackready.js";
4
4
  import type { Intent } from "./utils-transport.js";
5
+ import type { SyncStrategy } from "./storage/utils-storage.js";
5
6
 
6
7
  let {
7
8
  app,
8
9
  intents,
9
- shouldBackup,
10
+ getSyncStrategy,
10
11
  children,
11
12
  }: {
12
13
  app: string;
13
14
  intents?: Intent[];
14
- shouldBackup: (path: string) => boolean;
15
+ getSyncStrategy: (path: string) => SyncStrategy;
15
16
  children: Snippet;
16
17
  } = $props();
17
18
 
@@ -19,7 +20,7 @@
19
20
  const ready = trackReady(
20
21
  app,
21
22
  intents || [],
22
- shouldBackup,
23
+ getSyncStrategy,
23
24
  (startBackup) =>
24
25
  (backup = () => {
25
26
  startBackup();
@@ -1,9 +1,10 @@
1
1
  import type { Snippet } from "svelte";
2
2
  import type { Intent } from "./utils-transport.js";
3
+ import type { SyncStrategy } from "./storage/utils-storage.js";
3
4
  type $$ComponentProps = {
4
5
  app: string;
5
6
  intents?: Intent[];
6
- shouldBackup: (path: string) => boolean;
7
+ getSyncStrategy: (path: string) => SyncStrategy;
7
8
  children: Snippet;
8
9
  };
9
10
  declare const Monoidentity: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1,2 +1,3 @@
1
1
  import type { Bucket } from "../utils-transport.js";
2
- export declare const backupCloud: (shouldBackup: (path: string) => boolean, bucket: Bucket) => Promise<void>;
2
+ import { type SyncStrategy } from "./utils-storage.js";
3
+ export declare const backupCloud: (getSyncStrategy: (path: string) => SyncStrategy, bucket: Bucket) => Promise<void>;
@@ -1,10 +1,10 @@
1
1
  import { AwsClient } from "aws4fetch";
2
2
  import { storageClient, STORAGE_EVENT } from "./storageclient.svelte.js";
3
3
  import { addToSync } from "../storage.js";
4
- import { shouldPersist } from "./_should.js";
4
+ import { shouldPersist, enqueueSync } from "./utils-storage.js";
5
5
  const CLOUD_CACHE_KEY = "monoidentity-x/cloud-cache";
6
6
  let unmount;
7
- const loadFromCloud = async (shouldBackup, base, client) => {
7
+ const loadFromCloud = async (getSyncStrategy, base, client) => {
8
8
  const listResp = await client.fetch(base);
9
9
  if (!listResp.ok)
10
10
  throw new Error(`List bucket failed: ${listResp.status}`);
@@ -12,7 +12,7 @@ const loadFromCloud = async (shouldBackup, base, client) => {
12
12
  const objects = [...listXml.matchAll(/<Key>(.*?)<\/Key>.*?<ETag>(.*?)<\/ETag>/gs)]
13
13
  .map((m) => m.slice(1).map((s) => s.replaceAll("&quot;", `"`).replaceAll("&apos;", `'`)))
14
14
  .map(([key, etag]) => ({ key, etag: etag.replaceAll(`"`, "") }))
15
- .filter(({ key }) => shouldBackup(key));
15
+ .filter(({ key }) => getSyncStrategy(key).mode != "none");
16
16
  const prevCache = JSON.parse(localStorage[CLOUD_CACHE_KEY] || "{}");
17
17
  const nextCache = {};
18
18
  const model = {};
@@ -45,8 +45,8 @@ const loadFromCloud = async (shouldBackup, base, client) => {
45
45
  localStorage[CLOUD_CACHE_KEY] = JSON.stringify(nextCache);
46
46
  return model;
47
47
  };
48
- const syncFromCloud = async (shouldBackup, bucket, client) => {
49
- const remote = await loadFromCloud(shouldBackup, bucket.base, client);
48
+ const syncFromCloud = async (getSyncStrategy, bucket, client) => {
49
+ const remote = await loadFromCloud(getSyncStrategy, bucket.base, client);
50
50
  const local = storageClient();
51
51
  for (const key of Object.keys(local)) {
52
52
  if (key in remote)
@@ -61,17 +61,18 @@ const syncFromCloud = async (shouldBackup, bucket, client) => {
61
61
  local[key] = value;
62
62
  }
63
63
  };
64
- export const backupCloud = async (shouldBackup, bucket) => {
64
+ export const backupCloud = async (getSyncStrategy, bucket) => {
65
65
  unmount?.();
66
66
  const client = new AwsClient({
67
67
  accessKeyId: bucket.accessKeyId,
68
68
  secretAccessKey: bucket.secretAccessKey,
69
69
  });
70
- await syncFromCloud(shouldBackup, bucket, client);
71
- const syncIntervalId = setInterval(() => syncFromCloud(shouldBackup, bucket, client), 15 * 60 * 1000);
70
+ await syncFromCloud(getSyncStrategy, bucket, client);
71
+ const syncIntervalId = setInterval(() => syncFromCloud(getSyncStrategy, bucket, client), 15 * 60 * 1000);
72
72
  // Continuous sync: mirror local changes to cloud
73
73
  const write = async (key, value) => {
74
- if (!shouldBackup(key)) {
74
+ const strategy = getSyncStrategy(key);
75
+ if (strategy.mode == "none") {
75
76
  if (!shouldPersist(key))
76
77
  console.warn("[monoidentity cloud]", key, "isn't marked to be backed up or saved");
77
78
  return;
@@ -105,15 +106,23 @@ export const backupCloud = async (shouldBackup, bucket) => {
105
106
  localStorage[CLOUD_CACHE_KEY] = JSON.stringify(cache);
106
107
  }
107
108
  };
109
+ const writeWrapped = async (key, value) => write(key, value).catch((err) => {
110
+ console.error("[monoidentity cloud] save failed", key, err);
111
+ });
108
112
  const listener = (event) => {
109
- let key = event.detail.key;
110
- if (!key.startsWith("monoidentity/"))
113
+ const fullKey = event.detail.key;
114
+ if (!fullKey.startsWith("monoidentity/"))
115
+ return;
116
+ const key = fullKey.slice("monoidentity/".length);
117
+ const strategy = getSyncStrategy(key);
118
+ if (strategy.mode == "none")
111
119
  return;
112
- key = key.slice("monoidentity/".length);
113
- const promise = write(key, event.detail.value).catch((err) => {
114
- console.warn("[monoidentity cloud] save failed", key, err);
115
- });
116
- addToSync(promise);
120
+ if (strategy.mode == "immediate") {
121
+ addToSync(writeWrapped(key, event.detail.value));
122
+ }
123
+ else if (strategy.mode == "debounced") {
124
+ enqueueSync(fullKey, strategy.debounceMs, () => writeWrapped(key, localStorage[fullKey]));
125
+ }
117
126
  };
118
127
  addEventListener(STORAGE_EVENT, listener);
119
128
  unmount = () => {
@@ -1 +1,2 @@
1
- export declare const backupLocally: (shouldBackup: (path: string) => boolean, requestBackup: (startBackup: () => void) => void) => Promise<void>;
1
+ import { type SyncStrategy } from "./utils-storage.js";
2
+ export declare const backupLocally: (getSyncStrategy: (path: string) => SyncStrategy, requestBackup: (startBackup: () => void) => void) => Promise<void>;
@@ -1,9 +1,9 @@
1
1
  import { createStore, get, set } from "idb-keyval";
2
2
  import { STORAGE_EVENT, storageClient } from "./storageclient.svelte.js";
3
3
  import { canBackup } from "../utils-localstorage.js";
4
- import { shouldPersist } from "./_should.js";
4
+ import { shouldPersist } from "./utils-storage.js";
5
5
  let unmount;
6
- const saveToDir = (shouldBackup, dir) => {
6
+ const saveToDir = (getSyncStrategy, dir) => {
7
7
  let dirCache = {};
8
8
  const getDirCached = async (route) => {
9
9
  let key = "";
@@ -18,19 +18,9 @@ const saveToDir = (shouldBackup, dir) => {
18
18
  }
19
19
  return parent;
20
20
  };
21
- const listener = async (event) => {
22
- let key = event.detail.key;
23
- if (!key.startsWith("monoidentity/"))
24
- return;
25
- key = key.slice("monoidentity/".length);
21
+ const writeFile = async (key, value) => {
26
22
  const pathParts = key.split("/");
27
23
  const name = pathParts.at(-1);
28
- const value = event.detail.value;
29
- if (!shouldBackup(key)) {
30
- if (!shouldPersist(key))
31
- console.warn("[monoidentity backup]", key, "isn't marked to be backed up or saved");
32
- return;
33
- }
34
24
  console.debug("[monoidentity backup] saving", name);
35
25
  const parent = await getDirCached(pathParts.slice(0, -1));
36
26
  if (value != undefined) {
@@ -43,12 +33,26 @@ const saveToDir = (shouldBackup, dir) => {
43
33
  await parent.removeEntry(name);
44
34
  }
45
35
  };
36
+ const listener = async (event) => {
37
+ const fullKey = event.detail.key;
38
+ if (!fullKey.startsWith("monoidentity/"))
39
+ return;
40
+ const key = fullKey.slice("monoidentity/".length);
41
+ const strategy = getSyncStrategy(key);
42
+ if (strategy.mode == "none") {
43
+ if (!shouldPersist(key))
44
+ console.warn("[monoidentity backup]", key, "isn't marked to be backed up or saved");
45
+ return;
46
+ }
47
+ // Directly write
48
+ await writeFile(key, event.detail.value);
49
+ };
46
50
  addEventListener(STORAGE_EVENT, listener);
47
51
  return () => {
48
52
  removeEventListener(STORAGE_EVENT, listener);
49
53
  };
50
54
  };
51
- export const backupLocally = async (shouldBackup, requestBackup) => {
55
+ export const backupLocally = async (getSyncStrategy, requestBackup) => {
52
56
  if (!canBackup)
53
57
  return;
54
58
  if (localStorage["monoidentity-x/backup"] == "off")
@@ -59,7 +63,7 @@ export const backupLocally = async (shouldBackup, requestBackup) => {
59
63
  const dir = await get("backup", handles);
60
64
  if (!dir)
61
65
  throw new Error("No backup handle found");
62
- unmount = saveToDir(shouldBackup, dir);
66
+ unmount = saveToDir(getSyncStrategy, dir);
63
67
  }
64
68
  else {
65
69
  localStorage["monoidentity-x/backup"] = "off";
@@ -91,7 +95,7 @@ export const backupLocally = async (shouldBackup, requestBackup) => {
91
95
  location.reload();
92
96
  return;
93
97
  }
94
- unmount = saveToDir(shouldBackup, dir);
98
+ unmount = saveToDir(getSyncStrategy, dir);
95
99
  });
96
100
  }
97
101
  };
@@ -1,3 +1,4 @@
1
+ import { flush } from "./utils-storage.js";
1
2
  export const STORAGE_EVENT = "monoidentity-storage";
2
3
  const announce = (key, value) => {
3
4
  // Announce to all, even third parties
@@ -25,10 +26,36 @@ export const storageClient = (prefix, unprefix, serialize, deserialize) => {
25
26
  else {
26
27
  prefix = (key) => `monoidentity/${key}`;
27
28
  }
29
+ const getScopedKeys = () => {
30
+ const keys = [];
31
+ for (const key in localStorage) {
32
+ if (!key.startsWith("monoidentity/"))
33
+ continue;
34
+ let scopedKey = key.slice("monoidentity/".length);
35
+ if (unprefix) {
36
+ const unprefixed = unprefix(scopedKey);
37
+ if (!unprefixed)
38
+ continue;
39
+ scopedKey = unprefixed;
40
+ }
41
+ keys.push(scopedKey);
42
+ }
43
+ return keys;
44
+ };
28
45
  return new Proxy({}, {
29
46
  get(_, key) {
30
47
  if (typeof key == "symbol")
31
48
  return undefined;
49
+ if (key == "flush") {
50
+ return async (userKey) => {
51
+ if (userKey) {
52
+ await flush([prefix(userKey)]);
53
+ }
54
+ else {
55
+ await flush(getScopedKeys().map((k) => prefix(k)));
56
+ }
57
+ };
58
+ }
32
59
  key = prefix(key);
33
60
  storageCounters[key];
34
61
  const raw = localStorage[key];
@@ -50,20 +77,7 @@ export const storageClient = (prefix, unprefix, serialize, deserialize) => {
50
77
  },
51
78
  ownKeys() {
52
79
  allCounter;
53
- const keys = [];
54
- for (let key in localStorage) {
55
- if (!key.startsWith("monoidentity/"))
56
- continue;
57
- key = key.slice("monoidentity/".length);
58
- if (unprefix) {
59
- const unprefixed = unprefix(key);
60
- if (!unprefixed)
61
- continue;
62
- key = unprefixed;
63
- }
64
- keys.push(key);
65
- }
66
- return keys;
80
+ return getScopedKeys();
67
81
  },
68
82
  getOwnPropertyDescriptor(_, key) {
69
83
  key = prefix(key);
@@ -0,0 +1,11 @@
1
+ export declare const shouldPersist: (key: string) => boolean;
2
+ export type SyncStrategy = {
3
+ mode: "none";
4
+ } | {
5
+ mode: "immediate";
6
+ } | {
7
+ mode: "debounced";
8
+ debounceMs: number;
9
+ };
10
+ export declare const enqueueSync: (key: string, debounceMs: number, sync: () => Promise<void>) => void;
11
+ export declare const flush: (keys: string[]) => Promise<void>;
@@ -0,0 +1,38 @@
1
+ export const shouldPersist = (key) => key.includes(".cache/");
2
+ const syncQueue = {};
3
+ export const enqueueSync = (key, debounceMs, sync) => {
4
+ // Cancel existing timeout if any
5
+ if (syncQueue[key]) {
6
+ clearTimeout(syncQueue[key].timeout);
7
+ }
8
+ // Set new timeout
9
+ const timeout = setTimeout(() => {
10
+ const entry = syncQueue[key];
11
+ if (!entry)
12
+ throw new Error("Sync entry missing");
13
+ delete syncQueue[key];
14
+ entry.sync();
15
+ }, debounceMs);
16
+ syncQueue[key] = { timeout, sync };
17
+ };
18
+ export const flush = async (keys) => {
19
+ const entries = keys
20
+ .map((key) => [key, syncQueue[key]])
21
+ .filter((item) => Boolean(item[1]));
22
+ // Clear queue first
23
+ for (const [k, entry] of entries) {
24
+ clearTimeout(entry.timeout);
25
+ delete syncQueue[k];
26
+ }
27
+ // Then flush all
28
+ await Promise.all(entries.map(([_, entry]) => entry.sync()));
29
+ };
30
+ if (import.meta.hot) {
31
+ import.meta.hot.dispose(() => {
32
+ for (const key in syncQueue) {
33
+ const entry = syncQueue[key];
34
+ delete syncQueue[key];
35
+ clearTimeout(entry.timeout);
36
+ }
37
+ });
38
+ }
package/dist/storage.js CHANGED
@@ -43,5 +43,8 @@ export const setVerification = (jwt) => {
43
43
  const client = storageClient();
44
44
  client[VERIFICATION_PATH] = jwt;
45
45
  };
46
- export const getStorage = (realm) => storageClient((key) => `.${realm}/${app}/${key}.devalue`, undefined, stringify, parse);
46
+ export const getStorage = (realm) => {
47
+ const prefix = `.${realm}/${app}/`;
48
+ return storageClient((key) => `${prefix}${key}.devalue`, (key) => (key.startsWith(prefix) ? key.slice(prefix.length, -".devalue".length) : undefined), stringify, parse);
49
+ };
47
50
  export const getScopedFS = (dir) => storageClient((key) => `${dir}/${key}`, (key) => (key.startsWith(dir + "/") ? key.slice(dir.length + 1) : undefined));
@@ -1,2 +1,3 @@
1
1
  import { type Intent } from "./utils-transport.js";
2
- export declare const trackReady: (app: string, intents: Intent[], shouldBackup: (path: string) => boolean, requestBackup: (startBackup: () => void) => void) => Promise<void>;
2
+ import type { SyncStrategy } from "./storage/utils-storage.js";
3
+ export declare const trackReady: (app: string, intents: Intent[], getSyncStrategy: (path: string) => SyncStrategy, requestBackup: (startBackup: () => void) => void) => Promise<void>;
@@ -6,7 +6,7 @@ import { conf, setLoginRecognized } from "./storage.js";
6
6
  import { backupLocally } from "./storage/backuplocally.js";
7
7
  import { backupCloud } from "./storage/backupcloud.js";
8
8
  import { switchToHub } from "./utils-hub.js";
9
- export const trackReady = async (app, intents, shouldBackup, requestBackup) => {
9
+ export const trackReady = async (app, intents, getSyncStrategy, requestBackup) => {
10
10
  conf(app);
11
11
  let setup = localStorage["monoidentity-x/setup"]
12
12
  ? JSON.parse(localStorage["monoidentity-x/setup"])
@@ -28,10 +28,10 @@ export const trackReady = async (app, intents, shouldBackup, requestBackup) => {
28
28
  switchToHub([{ storage: true }, ...intents]);
29
29
  }
30
30
  if (setup.method == "localStorage") {
31
- await backupLocally(shouldBackup, requestBackup);
31
+ await backupLocally(getSyncStrategy, requestBackup);
32
32
  }
33
33
  if (setup.method == "cloud") {
34
- await backupCloud(shouldBackup, setup);
34
+ await backupCloud(getSyncStrategy, setup);
35
35
  }
36
36
  for (const provision of provisions) {
37
37
  if ("createLoginRecognized" in provision) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monoidentity",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "license": "ISC",
5
5
  "repository": "KTibow/monoidentity",
6
6
  "author": {
@@ -33,24 +33,24 @@
33
33
  "dependencies": {
34
34
  "@tsndr/cloudflare-worker-jwt": "^3.2.0",
35
35
  "aws4fetch": "^1.0.20",
36
- "devalue": "^5.4.2",
37
- "fast-studentvue": "^2.0.3",
36
+ "devalue": "^5.5.0",
37
+ "fast-studentvue": "^2.1.1",
38
38
  "idb-keyval": "^6.2.2",
39
39
  "valibot": "^1.1.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@sveltejs/adapter-static": "^3.0.10",
43
- "@sveltejs/kit": "^2.48.4",
43
+ "@sveltejs/kit": "^2.48.5",
44
44
  "@sveltejs/package": "^2.5.4",
45
45
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
46
46
  "@types/wicg-file-system-access": "^2023.10.7",
47
- "knip": "^5.68.0",
47
+ "knip": "^5.69.1",
48
48
  "monoserve": "^3.2.2",
49
49
  "publint": "^0.3.15",
50
- "rolldown": "1.0.0-beta.47",
51
- "school-districts": "^4.0.0",
52
- "svelte": "^5.43.5",
53
- "svelte-check": "^4.3.3",
50
+ "rolldown": "^1.0.0-beta.47",
51
+ "school-districts": "^5.0.1",
52
+ "svelte": "^5.43.6",
53
+ "svelte-check": "^4.3.4",
54
54
  "tinyglobby": "^0.2.15",
55
55
  "typescript": "^5.9.3",
56
56
  "vite": "^7.2.2"
@@ -1 +0,0 @@
1
- export declare const shouldPersist: (key: string) => boolean;
@@ -1 +0,0 @@
1
- export const shouldPersist = (key) => key.includes(".cache/");