monoidentity 0.0.4 → 0.2.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.
@@ -6,11 +6,82 @@
6
6
  let { app, scopes, children }: { app: string; scopes: Scope[]; children: Snippet } = $props();
7
7
 
8
8
  let ready = $state(false);
9
- trackReady(app, scopes, () => (ready = true));
9
+ let backup: (() => void) | undefined = $state();
10
+ trackReady(
11
+ app,
12
+ scopes,
13
+ (callback) => {
14
+ backup = () => {
15
+ callback();
16
+ backup = undefined;
17
+ };
18
+ },
19
+ () => (ready = true),
20
+ );
10
21
  </script>
11
22
 
23
+ {#snippet backupUI(yes: () => void, no: () => void)}
24
+ <p>Avoid reconfiguration with a backup folder.</p>
25
+ <div class="buttons">
26
+ <button class="primary" onclick={yes}>Connect</button>
27
+ <button onclick={no}>Skip</button>
28
+ </div>
29
+ {/snippet}
30
+
12
31
  {#if ready}
13
32
  {@render children()}
33
+ {#if backup}
34
+ <div class="backup toast">
35
+ {@render backupUI(backup, () => (backup = undefined))}
36
+ </div>
37
+ {/if}
38
+ {:else if backup}
39
+ <div class="backup center">
40
+ {@render backupUI(backup, () => (backup = undefined))}
41
+ </div>
14
42
  {:else}
15
- <p style:margin="auto">Setting up</p>
43
+ <p class="center">Setting up</p>
16
44
  {/if}
45
+
46
+ <style>
47
+ .backup {
48
+ display: flex;
49
+ flex-direction: column;
50
+ gap: 0.5rem;
51
+
52
+ background-color: light-dark(#000, #fff);
53
+ color: light-dark(#fff, #000);
54
+ line-height: 1;
55
+ padding: 0.75rem;
56
+ border-radius: 1.5rem;
57
+
58
+ > .buttons {
59
+ display: flex;
60
+ gap: 0.5rem;
61
+ > button {
62
+ display: flex;
63
+ flex: 1;
64
+ align-items: center;
65
+ justify-content: center;
66
+ height: 2rem;
67
+ border-radius: 0.75rem;
68
+ &.primary {
69
+ background-color: light-dark(#fff, #000);
70
+ color: light-dark(#000, #fff);
71
+ }
72
+ border: none;
73
+ font: inherit;
74
+ cursor: pointer;
75
+ }
76
+ }
77
+ }
78
+ .toast {
79
+ position: fixed;
80
+ right: 1rem;
81
+ top: 1rem;
82
+ z-index: 1000;
83
+ }
84
+ .center {
85
+ margin: auto;
86
+ }
87
+ </style>
@@ -0,0 +1,10 @@
1
+ export type Dict = Record<string, string>;
2
+ type ProxyHandlerWithoutTarget = {
3
+ has?(p: string | symbol): boolean;
4
+ get?(p: string | symbol, receiver: any): any;
5
+ set?(p: string | symbol, newValue: any, receiver: any): boolean;
6
+ deleteProperty?(p: string | symbol): boolean;
7
+ ownKeys?(): ArrayLike<string | symbol>;
8
+ };
9
+ export declare const createStore: (implementation: ProxyHandlerWithoutTarget) => Dict;
10
+ export {};
@@ -0,0 +1,9 @@
1
+ export const createStore = (implementation) => {
2
+ const target = {};
3
+ const handler = {};
4
+ for (const key of Object.keys(implementation)) {
5
+ const trap = implementation[key];
6
+ handler[key] = (_, ...args) => trap(...args);
7
+ }
8
+ return new Proxy(target, handler);
9
+ };
@@ -0,0 +1,3 @@
1
+ import { type Dict } from "./_createstore.js";
2
+ export declare const init: () => Dict;
3
+ export declare const wrapWithBackup: (storage: Dict, requestBackup: (callback: () => void) => void) => Dict;
@@ -0,0 +1,134 @@
1
+ import { createStore } from "./_createstore.js";
2
+ import { wrapWithReplay } from "./_replay.js";
3
+ import { canBackup } from "./utils-callback.js";
4
+ import { openDB } from "idb";
5
+ const prefix = "monoidentity/";
6
+ const prefixed = (key) => `${prefix}${key}`;
7
+ const unprefixed = (key) => {
8
+ if (!key.startsWith(prefix))
9
+ throw new Error("Key is not prefixed");
10
+ return key.slice(prefix.length);
11
+ };
12
+ const get = (key) => {
13
+ if (typeof key != "string")
14
+ return undefined;
15
+ const value = localStorage.getItem(prefixed(key));
16
+ if (value == null)
17
+ return undefined;
18
+ return value;
19
+ };
20
+ export const init = () => createStore({
21
+ has(key) {
22
+ return typeof key == "string" && localStorage.getItem(prefixed(key)) !== null;
23
+ },
24
+ get,
25
+ set(key, value) {
26
+ if (typeof key == "string") {
27
+ localStorage.setItem(prefixed(key), value);
28
+ return true;
29
+ }
30
+ return false;
31
+ },
32
+ deleteProperty(key) {
33
+ if (typeof key == "string") {
34
+ localStorage.removeItem(prefixed(key));
35
+ return true;
36
+ }
37
+ return false;
38
+ },
39
+ ownKeys() {
40
+ const keys = [];
41
+ for (let i = 0; i < localStorage.length; i++) {
42
+ const key = localStorage.key(i);
43
+ if (key && key.startsWith(prefix)) {
44
+ keys.push(unprefixed(key));
45
+ }
46
+ }
47
+ return keys;
48
+ },
49
+ });
50
+ export const wrapWithBackup = (storage, requestBackup) => {
51
+ if (!canBackup)
52
+ return storage;
53
+ if (localStorage["monoidentity-x/backup"] == "off")
54
+ return storage;
55
+ const { proxy, flush, setTransmit, load } = wrapWithReplay(storage);
56
+ const getDir = async () => {
57
+ const db = await openDB("monoidentity-x");
58
+ const handle = (await db.get("handles", "backup"));
59
+ if (!handle)
60
+ throw new Error("No backup handle found");
61
+ return handle;
62
+ };
63
+ const setDir = async (dir) => {
64
+ const db = await openDB("monoidentity-x", 1, {
65
+ upgrade(db) {
66
+ db.createObjectStore("handles");
67
+ },
68
+ });
69
+ await db.put("handles", dir, "backup");
70
+ };
71
+ const init = async (dir) => {
72
+ let dirCache = {};
73
+ const getDirCached = async (route) => {
74
+ let key = "";
75
+ let parent = dir;
76
+ for (const path of route) {
77
+ key += "/";
78
+ key += path;
79
+ if (!dirCache[key]) {
80
+ dirCache[key] = await parent.getDirectoryHandle(path, { create: true });
81
+ }
82
+ parent = dirCache[key];
83
+ }
84
+ return parent;
85
+ };
86
+ setTransmit(async (path, mod) => {
87
+ const pathParts = path.split("/");
88
+ const parent = await getDirCached(pathParts.slice(0, -1));
89
+ if (mod.type == "set") {
90
+ const file = await parent.getFileHandle(pathParts.at(-1), { create: true });
91
+ const writable = await file.createWritable();
92
+ await writable.write(mod.new);
93
+ await writable.close();
94
+ }
95
+ else if (mod.type == "delete") {
96
+ await parent.removeEntry(pathParts.at(-1));
97
+ }
98
+ });
99
+ await flush();
100
+ };
101
+ if (localStorage["monoidentity-x/backup"] == "on") {
102
+ getDir()
103
+ .then((dir) => init(dir))
104
+ .catch(() => {
105
+ delete localStorage["monoidentity-x/backup"];
106
+ });
107
+ }
108
+ else {
109
+ localStorage["monoidentity-x/backup"] = "off";
110
+ requestBackup(async () => {
111
+ const dir = await showDirectoryPicker({ mode: "readwrite" });
112
+ await setDir(dir);
113
+ await init(dir);
114
+ // Restore from backup
115
+ const backup = {};
116
+ const traverse = async (d, path) => {
117
+ for await (const entry of d.values()) {
118
+ if (entry.kind == "file") {
119
+ const file = await entry.getFile();
120
+ const text = await file.text();
121
+ backup[`${path}${entry.name}`] = text;
122
+ }
123
+ else if (entry.kind == "directory") {
124
+ await traverse(entry, `${path}${entry.name}/`);
125
+ }
126
+ }
127
+ };
128
+ await traverse(dir, "");
129
+ await load(backup);
130
+ localStorage["monoidentity-x/backup"] = "on";
131
+ });
132
+ }
133
+ return proxy;
134
+ };
@@ -0,0 +1,16 @@
1
+ import type { Dict } from "./_createstore.js";
2
+ type Modification = {
3
+ type: "set";
4
+ old?: string;
5
+ new: string;
6
+ } | {
7
+ type: "delete";
8
+ };
9
+ type Transmit = (path: string, mod: Modification) => Promise<void>;
10
+ export declare const wrapWithReplay: (storage: Dict) => {
11
+ proxy: Dict;
12
+ flush: () => Promise<void>;
13
+ setTransmit(tx: Transmit): void;
14
+ load(external: Dict): Promise<void>;
15
+ };
16
+ export {};
@@ -0,0 +1,97 @@
1
+ import { throttle } from "./utils-timing.js";
2
+ export const wrapWithReplay = (storage) => {
3
+ let modifications = {};
4
+ if (localStorage["monoidentity-x/backup/modifications"]) {
5
+ modifications = JSON.parse(localStorage["monoidentity-x/backup/modifications"]);
6
+ }
7
+ const save = () => {
8
+ if (Object.keys(modifications).length) {
9
+ localStorage["monoidentity-x/backup/modifications"] = JSON.stringify(modifications);
10
+ }
11
+ else {
12
+ delete localStorage["monoidentity-x/backup/modifications"];
13
+ }
14
+ };
15
+ window.addEventListener("beforeunload", save);
16
+ window.addEventListener("pagehide", save);
17
+ let transmit;
18
+ const flush = throttle(async () => {
19
+ try {
20
+ const tx = transmit;
21
+ if (!tx)
22
+ return;
23
+ const paths = Object.keys(modifications);
24
+ const tasks = paths
25
+ .map(async (path) => {
26
+ const mod = modifications[path];
27
+ await tx(path, mod);
28
+ })
29
+ .map((task, i) => task
30
+ .then(() => {
31
+ const path = paths[i];
32
+ delete modifications[path];
33
+ })
34
+ .catch((err) => {
35
+ const path = paths[i];
36
+ console.warn("[monoidentity] transmit failed, will retry later", { path, err });
37
+ }));
38
+ await Promise.all(tasks);
39
+ }
40
+ finally {
41
+ save();
42
+ }
43
+ }, 1000);
44
+ const proxy = new Proxy(storage, {
45
+ set(target, prop, value) {
46
+ let old = target[prop];
47
+ target[prop] = value;
48
+ const oldMod = modifications[prop];
49
+ if (oldMod?.type == "set")
50
+ old = oldMod.old;
51
+ modifications[prop] = { type: "set", old, new: value };
52
+ flush();
53
+ return true;
54
+ },
55
+ deleteProperty(target, prop) {
56
+ const success = delete target[prop];
57
+ if (success) {
58
+ modifications[prop] = { type: "delete" };
59
+ flush();
60
+ }
61
+ return success;
62
+ },
63
+ });
64
+ return {
65
+ proxy,
66
+ flush,
67
+ setTransmit(tx) {
68
+ transmit = tx;
69
+ },
70
+ // load() just resets to the external version
71
+ // if you have modifications, flush() them first
72
+ async load(external) {
73
+ modifications = {};
74
+ for (const [key, value] of Object.entries(external)) {
75
+ storage[key] = value;
76
+ }
77
+ for (const key of Object.keys(storage)) {
78
+ if (!(key in external)) {
79
+ delete storage[key];
80
+ }
81
+ }
82
+ // // If for *some* reason, there are pending modifications, rebase and resend them
83
+ // for (const [key, mod] of Object.entries(oldModifications)) {
84
+ // if (mod.type == "set") {
85
+ // if (proxy[key] != mod.old)
86
+ // console.warn(
87
+ // `[monoidentity] modification to "${key}" will be force applied over external change`,
88
+ // );
89
+ // proxy[key] = mod.new;
90
+ // } else if (mod.type == "delete") {
91
+ // delete proxy[key];
92
+ // }
93
+ // }
94
+ // flush();
95
+ },
96
+ };
97
+ };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { default as Monoidentity } from "./Monoidentity.svelte";
2
2
  export { getLogin, getStorage } from "./storage.js";
3
3
  export * from "./trackready.js";
4
+ export { encode, decode } from "./utils-base36.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { default as Monoidentity } from "./Monoidentity.svelte";
2
2
  export { getLogin, getStorage } from "./storage.js";
3
3
  export * from "./trackready.js";
4
+ export { encode, decode } from "./utils-base36.js";
package/dist/storage.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Login } from "./utils-login.js";
1
+ import { type Login } from "./utils-base36.js";
2
2
  export declare const setup: (i: Record<string, string>, a: string) => void;
3
3
  export declare const getLogin: () => Login;
4
- export declare const getStorage: (realm: "cache") => Record<string, any>;
4
+ export declare const getStorage: (realm: "cache") => import("./_createstore.js").Dict;
package/dist/storage.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { stringify, parse } from "devalue";
2
- import { decode } from "./utils-login.js";
2
+ import { decode } from "./utils-base36.js";
3
+ import { createStore } from "./_createstore.js";
3
4
  let implementation;
4
5
  let app = "";
5
6
  export const setup = (i, a) => {
@@ -7,30 +8,30 @@ export const setup = (i, a) => {
7
8
  app = a;
8
9
  };
9
10
  export const getLogin = () => {
10
- const storage = implementation;
11
- if (!storage)
11
+ if (!implementation)
12
12
  throw new Error("No implementation set");
13
- const login = storage[".core/login.encjson"];
13
+ const login = implementation[".core/login.encjson"];
14
14
  if (!login)
15
15
  throw new Error("No login found");
16
16
  return JSON.parse(decode(login));
17
17
  };
18
18
  export const getStorage = (realm) => {
19
19
  const prefix = (text) => `.${realm}/${app}/${text}`;
20
- const storage = implementation;
21
20
  if (!app)
22
21
  throw new Error("No app set");
23
- if (!storage)
24
- throw new Error("No implementation set");
25
- return new Proxy({}, {
26
- get(_, key) {
27
- const item = storage[prefix(key)];
22
+ return createStore({
23
+ get(key) {
24
+ if (!implementation)
25
+ throw new Error("No implementation set");
26
+ const item = implementation[prefix(key)];
28
27
  if (!item)
29
28
  return undefined;
30
29
  return parse(item);
31
30
  },
32
- set(_, key, value) {
33
- storage[prefix(key)] = stringify(value);
31
+ set(key, value) {
32
+ if (!implementation)
33
+ throw new Error("No implementation set");
34
+ implementation[prefix(key)] = stringify(value);
34
35
  return true;
35
36
  },
36
37
  });
@@ -1,2 +1,2 @@
1
1
  import { type Scope } from "./utils-scope.js";
2
- export declare const trackReady: (app: string, scopes: Scope[], callback: () => void) => void;
2
+ export declare const trackReady: (app: string, scopes: Scope[], requestBackup: (callback: () => void) => void, callback: () => void) => void;
@@ -1,24 +1,20 @@
1
1
  import { rememberCallback } from "./utils-callback.js";
2
2
  import {} from "./utils-scope.js";
3
- import { init as initLocal } from "./storage-private-local.js";
3
+ import { init as initLocal, wrapWithBackup } from "./_localstorage.js";
4
4
  import { setup } from "./storage.js";
5
- export const trackReady = (app, scopes, callback) => {
5
+ export const trackReady = (app, scopes, requestBackup, callback) => {
6
6
  const params = new URLSearchParams(location.hash.slice(1));
7
7
  let memory = localStorage.monoidentityMemory
8
8
  ? JSON.parse(localStorage.monoidentityMemory)
9
9
  : undefined;
10
- let createNew = false;
11
10
  let fileTasks = undefined;
12
11
  const paramCB = params.get("monoidentitycallback");
13
12
  if (paramCB) {
14
13
  history.replaceState(null, "", location.pathname);
15
14
  const cb = JSON.parse(paramCB);
16
- console.log("got callback", cb); // todo remove
15
+ // console.debug("[monoidentity] callback", cb);
17
16
  memory = rememberCallback(cb, memory);
18
17
  localStorage.monoidentityMemory = JSON.stringify(memory);
19
- if (cb.connect.method == "file" && cb.connect.createNew) {
20
- createNew = true;
21
- }
22
18
  fileTasks = cb.fileTasks;
23
19
  }
24
20
  if (!memory) {
@@ -34,19 +30,18 @@ export const trackReady = (app, scopes, callback) => {
34
30
  // TODO
35
31
  throw new Error("unimplemented");
36
32
  }
37
- else if (memory.method == "file") {
38
- // TODO (use createNew here)
39
- throw new Error("unimplemented");
40
- }
41
33
  else if (memory.method == "localStorage") {
42
34
  storage = initLocal();
35
+ storage = wrapWithBackup(storage, requestBackup);
43
36
  }
44
37
  else {
45
38
  throw new Error("unreachable");
46
39
  }
47
40
  setup(storage, app);
48
- for (const file in fileTasks) {
49
- storage[file] = fileTasks[file];
41
+ if (fileTasks) {
42
+ for (const file in fileTasks) {
43
+ storage[file] = fileTasks[file];
44
+ }
50
45
  }
51
46
  callback();
52
47
  };
@@ -1,23 +1,17 @@
1
- export declare const supportsFile: boolean;
2
1
  type Setup = {
3
2
  method: "cloud";
4
3
  jwt: string;
5
- } | {
6
- method: "file";
7
- createNew?: never;
8
4
  } | {
9
5
  method: "localStorage";
10
6
  };
11
- export type Memory = Setup & {
12
- knownFiles: string[];
13
- };
7
+ export type Memory = Setup;
14
8
  export type Callback = {
15
9
  scopes: string[];
16
10
  connect: Setup | {
17
- method: "file";
18
- createNew: boolean;
11
+ method: "localStorage";
19
12
  };
20
13
  fileTasks: Record<string, string> | undefined;
21
14
  };
15
+ export declare const canBackup: boolean;
22
16
  export declare const rememberCallback: (data: Callback, pastMemory?: Memory) => Memory;
23
17
  export {};
@@ -1,27 +1,7 @@
1
- export const supportsFile = "showSaveFilePicker" in window;
1
+ export const canBackup = navigator.userAgent.includes("CrOS") && Boolean(window.showDirectoryPicker);
2
2
  export const rememberCallback = (data, pastMemory) => {
3
- const { scopes, connect, fileTasks } = data;
4
- const setup = connect.method == "cloud" ? { method: "cloud", jwt: connect.jwt } : { method: connect.method };
5
- let knownFilesSet = new Set();
6
- if (pastMemory) {
7
- for (const file of pastMemory.knownFiles) {
8
- knownFilesSet.add(file);
9
- }
10
- }
11
- if (fileTasks) {
12
- for (const file of Object.keys(fileTasks)) {
13
- knownFilesSet.add(file);
14
- }
15
- }
16
- if (scopes.includes("login-recognized")) {
17
- const path = ".core/login.encjson";
18
- if (connect.method == "cloud") {
19
- knownFilesSet.add(path);
20
- }
21
- else if (!knownFilesSet.has(path)) {
22
- console.warn("unexpected login deficit");
23
- }
24
- }
25
- const knownFiles = Array.from(knownFilesSet);
26
- return { ...setup, knownFiles };
3
+ const { connect } = data;
4
+ return connect.method == "cloud"
5
+ ? { method: "cloud", jwt: connect.jwt }
6
+ : { method: connect.method };
27
7
  };
@@ -0,0 +1 @@
1
+ export declare function throttle(fn: () => Promise<unknown>, delay: number): () => Promise<void>;
@@ -0,0 +1,21 @@
1
+ export function throttle(fn, delay) {
2
+ let isRunning = false;
3
+ let hasPendingCall = false;
4
+ return async () => {
5
+ hasPendingCall = true;
6
+ if (isRunning) {
7
+ return;
8
+ }
9
+ try {
10
+ isRunning = true;
11
+ while (hasPendingCall) {
12
+ hasPendingCall = false;
13
+ await new Promise((resolve) => setTimeout(resolve, delay));
14
+ await fn();
15
+ }
16
+ }
17
+ finally {
18
+ isRunning = false;
19
+ }
20
+ };
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monoidentity",
3
- "version": "0.0.4",
3
+ "version": "0.2.0",
4
4
  "repository": "KTibow/monoidentity",
5
5
  "author": {
6
6
  "name": "KTibow"
@@ -28,13 +28,15 @@
28
28
  "svelte": "^5.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "devalue": "^5.3.2"
31
+ "devalue": "^5.3.2",
32
+ "idb": "^8.0.3"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@sveltejs/adapter-static": "^3.0.9",
35
36
  "@sveltejs/kit": "^2.22.0",
36
37
  "@sveltejs/package": "^2.0.0",
37
38
  "@sveltejs/vite-plugin-svelte": "^6.0.0",
39
+ "@types/wicg-file-system-access": "^2023.10.6",
38
40
  "publint": "^0.3.2",
39
41
  "svelte": "^5.0.0",
40
42
  "svelte-check": "^4.0.0",
@@ -1 +0,0 @@
1
- export declare const init: () => Record<string, string>;
@@ -1,56 +0,0 @@
1
- const prefix = "monoidentity/";
2
- const prefixed = (key) => `${prefix}${key}`;
3
- const unprefixed = (key) => {
4
- if (!key.startsWith(prefix))
5
- throw new Error("Key is not prefixed");
6
- return key.slice(prefix.length);
7
- };
8
- const target = {};
9
- const get = (_, key) => {
10
- if (typeof key != "string")
11
- return undefined;
12
- const value = localStorage.getItem(prefixed(key));
13
- if (value == null)
14
- return undefined;
15
- return value;
16
- };
17
- export const init = () => new Proxy(target, {
18
- get,
19
- set(_, key, value) {
20
- if (typeof key == "string") {
21
- localStorage.setItem(prefixed(key), value);
22
- return true;
23
- }
24
- return false;
25
- },
26
- deleteProperty(_, key) {
27
- if (typeof key == "string") {
28
- localStorage.removeItem(prefixed(key));
29
- return true;
30
- }
31
- return false;
32
- },
33
- has(_, key) {
34
- return typeof key == "string" && localStorage.getItem(prefixed(key)) !== null;
35
- },
36
- ownKeys(_) {
37
- const keys = [];
38
- for (let i = 0; i < localStorage.length; i++) {
39
- const key = localStorage.key(i);
40
- if (key && key.startsWith(prefix)) {
41
- keys.push(unprefixed(key));
42
- }
43
- }
44
- return keys;
45
- },
46
- getOwnPropertyDescriptor(_, key) {
47
- if (typeof key == "string" && localStorage.getItem(prefixed(key)) !== null) {
48
- return {
49
- enumerable: true,
50
- configurable: true,
51
- value: get(target, key),
52
- };
53
- }
54
- return undefined;
55
- },
56
- });
File without changes
File without changes