sliftutils 0.1.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.
Files changed (47) hide show
  1. package/.cursorrules +161 -0
  2. package/.eslintrc.js +38 -0
  3. package/.vscode/settings.json +39 -0
  4. package/bundler/buffer.js +2370 -0
  5. package/bundler/bundleEntry.ts +32 -0
  6. package/bundler/bundleEntryCaller.ts +8 -0
  7. package/bundler/bundleRequire.ts +244 -0
  8. package/bundler/bundleWrapper.ts +115 -0
  9. package/bundler/bundler.ts +72 -0
  10. package/bundler/flattenSourceMaps.ts +0 -0
  11. package/bundler/sourceMaps.ts +261 -0
  12. package/misc/environment.ts +11 -0
  13. package/misc/types.ts +3 -0
  14. package/misc/zip.ts +37 -0
  15. package/package.json +24 -0
  16. package/spec.txt +33 -0
  17. package/storage/CachedStorage.ts +32 -0
  18. package/storage/DelayedStorage.ts +30 -0
  19. package/storage/DiskCollection.ts +272 -0
  20. package/storage/FileFolderAPI.tsx +427 -0
  21. package/storage/IStorage.ts +40 -0
  22. package/storage/IndexedDBFileFolderAPI.ts +170 -0
  23. package/storage/JSONStorage.ts +35 -0
  24. package/storage/PendingManager.tsx +63 -0
  25. package/storage/PendingStorage.ts +47 -0
  26. package/storage/PrivateFileSystemStorage.ts +192 -0
  27. package/storage/StorageObservable.ts +122 -0
  28. package/storage/TransactionStorage.ts +485 -0
  29. package/storage/fileSystemPointer.ts +81 -0
  30. package/storage/storage.d.ts +41 -0
  31. package/tsconfig.json +31 -0
  32. package/web/DropdownCustom.tsx +150 -0
  33. package/web/FullscreenModal.tsx +75 -0
  34. package/web/GenericFormat.tsx +186 -0
  35. package/web/Input.tsx +350 -0
  36. package/web/InputLabel.tsx +288 -0
  37. package/web/InputPicker.tsx +158 -0
  38. package/web/LocalStorageParam.ts +56 -0
  39. package/web/SyncedController.ts +405 -0
  40. package/web/SyncedLoadingIndicator.tsx +37 -0
  41. package/web/Table.tsx +188 -0
  42. package/web/URLParam.ts +84 -0
  43. package/web/asyncObservable.ts +40 -0
  44. package/web/colors.tsx +14 -0
  45. package/web/mobxTyped.ts +29 -0
  46. package/web/modal.tsx +18 -0
  47. package/web/observer.tsx +35 -0
@@ -0,0 +1,40 @@
1
+ // TODO: Create a IStorageKeyEscaped interface, which uses escaping so that any keys
2
+ // are allowed. At the moment if you use keys which the underlying storage (ex,
3
+ // the file system) doesn't support, it will just throw.
4
+
5
+ export type IStorageSync<T> = {
6
+ get(key: string): T | undefined;
7
+ set(key: string, value: T): void;
8
+ remove(key: string): void;
9
+ getKeys(): string[];
10
+ getValues(): T[];
11
+ getEntries(): [string, T][];
12
+ getInfo(key: string): { size: number; lastModified: number } | undefined;
13
+ reset(): Promise<void>;
14
+ };
15
+ export type IStorage<T> = {
16
+ get(key: string): Promise<T | undefined>;
17
+ set(key: string, value: T): Promise<void>;
18
+ remove(key: string): Promise<void>;
19
+ getKeys(): Promise<string[]>;
20
+ getInfo(key: string): Promise<undefined | {
21
+ size: number;
22
+ lastModified: number;
23
+ }>;
24
+ reset(): Promise<void>;
25
+ };
26
+ // NOTE: In the file system some characters are disallowed, and some characters do special things
27
+ // (/ makes a folder). And there are even more rules, such as lengths per folder, etc, etc.
28
+ export type IStorageRaw = {
29
+ get(key: string): Promise<Buffer | undefined>;
30
+ // May or may not be efficient in the underlying storage
31
+ append(key: string, value: Buffer): Promise<void>;
32
+ set(key: string, value: Buffer): Promise<void>;
33
+ remove(key: string): Promise<void>;
34
+ getKeys(): Promise<string[]>;
35
+ getInfo(key: string): Promise<undefined | {
36
+ size: number;
37
+ lastModified: number;
38
+ }>;
39
+ reset(): Promise<void>;
40
+ };
@@ -0,0 +1,170 @@
1
+ import { lazy } from "socket-function/src/caching";
2
+ import { IStorageRaw } from "./IStorage";
3
+ import { FileStorage } from "./FileFolderAPI";
4
+
5
+ const DB_NAME = "FileStorage";
6
+ const STORE_NAME = "files";
7
+ const DB_VERSION = 1;
8
+
9
+ interface FileRecord {
10
+ data: Buffer;
11
+ lastModified: number;
12
+ }
13
+
14
+ class VirtualFileStorage implements FileStorage {
15
+ private db: IDBDatabase;
16
+
17
+ constructor(
18
+ db: IDBDatabase,
19
+ public readonly id: string
20
+ ) {
21
+ if (!db) debugger;
22
+ this.db = db;
23
+ }
24
+
25
+ private getStore(mode: IDBTransactionMode = "readonly") {
26
+ const transaction = this.db.transaction(STORE_NAME, mode);
27
+ return transaction.objectStore(STORE_NAME);
28
+ }
29
+
30
+ private request<T>(request: IDBRequest<T>): Promise<T> {
31
+ return new Promise((resolve, reject) => {
32
+ request.onsuccess = () => resolve(request.result);
33
+ request.onerror = () => reject(request.error);
34
+ });
35
+ }
36
+
37
+
38
+ // IStorageRaw implementation
39
+ async get(key: string): Promise<Buffer | undefined> {
40
+ const store = this.getStore();
41
+ const result = await this.request<FileRecord | undefined>(store.get(this.id + key));
42
+ let badBuffer = result?.data;
43
+ if (badBuffer) badBuffer = Buffer.from(badBuffer);
44
+ return badBuffer;
45
+ }
46
+
47
+ async append(key: string, value: Buffer): Promise<void> {
48
+ const store = this.getStore("readwrite");
49
+ const fullPath = this.id + key;
50
+ const existing = await this.request<FileRecord | undefined>(store.get(fullPath));
51
+
52
+ const newRecord: FileRecord = {
53
+ data: existing
54
+ ? Buffer.concat([existing.data, value])
55
+ : value,
56
+ lastModified: Date.now()
57
+ };
58
+
59
+ await this.request(store.put(newRecord, fullPath));
60
+ }
61
+
62
+ async set(key: string, value: Buffer): Promise<void> {
63
+ const store = this.getStore("readwrite");
64
+ const record: FileRecord = {
65
+ data: value,
66
+ lastModified: Date.now()
67
+ };
68
+ await this.request(store.put(record, this.id + key));
69
+ }
70
+
71
+ async remove(key: string): Promise<void> {
72
+ const store = this.getStore("readwrite");
73
+ await this.request(store.delete(this.id + key));
74
+ }
75
+
76
+ private async getKeysWithPrefix(prefix: string): Promise<string[]> {
77
+ const store = this.getStore();
78
+ const range = IDBKeyRange.bound(prefix, prefix + "\uffff", false, true);
79
+
80
+ return new Promise((resolve, reject) => {
81
+ const keys: string[] = [];
82
+ const request = store.openCursor(range);
83
+
84
+ request.onerror = () => reject(request.error);
85
+ request.onsuccess = () => {
86
+ const cursor = request.result;
87
+ if (cursor) {
88
+ let newKey = cursor.key as string;
89
+ newKey = newKey.slice(this.id.length);
90
+ keys.push(newKey);
91
+ cursor.continue();
92
+ } else {
93
+ resolve(keys);
94
+ }
95
+ };
96
+ });
97
+ }
98
+
99
+ async getKeys(): Promise<string[]> {
100
+ let keys = await this.getKeysWithPrefix(this.id);
101
+ return keys.filter(x => !x.includes("/"));
102
+ }
103
+
104
+ async getInfo(key: string): Promise<{ size: number; lastModified: number; } | undefined> {
105
+ const store = this.getStore();
106
+ const result = await this.request<FileRecord | undefined>(store.get(this.id + key));
107
+
108
+ if (!result) return undefined;
109
+
110
+ return {
111
+ size: result.data.length,
112
+ lastModified: result.lastModified
113
+ };
114
+ }
115
+
116
+ async reset(): Promise<void> {
117
+ let keys = await this.getKeysWithPrefix(this.id);
118
+ for (let key of keys) {
119
+ await this.remove(key);
120
+ }
121
+ }
122
+
123
+ // NestedFileStorage implementation
124
+ folder = {
125
+ hasKey: async (key: string): Promise<boolean> => {
126
+ const folderPath = this.id + key + "/";
127
+ const keys = await this.getKeysWithPrefix(folderPath);
128
+ return keys.length > 0;
129
+ },
130
+
131
+ getStorage: async (key: string): Promise<FileStorage> => {
132
+ const newPath = this.id + key + "/";
133
+ return new VirtualFileStorage(this.db, newPath);
134
+ },
135
+
136
+ removeStorage: async (key: string): Promise<void> => {
137
+ let nested = new VirtualFileStorage(this.db, this.id + key + "/");
138
+ await nested.reset();
139
+ },
140
+
141
+ getKeys: async (): Promise<string[]> => {
142
+ let keys = await this.getKeysWithPrefix(this.id);
143
+ let folderKeys = new Set<string>();
144
+ for (let key of keys) {
145
+ if (!key.includes("/")) continue;
146
+ let parts = key.split("/");
147
+ folderKeys.add(parts[0]);
148
+ }
149
+ return Array.from(folderKeys);
150
+ }
151
+ };
152
+ }
153
+
154
+ export const getFileStorageIndexDB = lazy(async (): Promise<FileStorage> => {
155
+ const db = await new Promise<IDBDatabase>((resolve, reject) => {
156
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
157
+
158
+ request.onerror = () => reject(request.error);
159
+ request.onsuccess = () => resolve(request.result);
160
+
161
+ request.onupgradeneeded = (event) => {
162
+ const db = (event.target as IDBOpenDBRequest).result;
163
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
164
+ db.createObjectStore(STORE_NAME);
165
+ }
166
+ };
167
+ });
168
+
169
+ return new VirtualFileStorage(db, "/");
170
+ });
@@ -0,0 +1,35 @@
1
+ import { IStorage } from "./IStorage";
2
+
3
+ export class JSONStorage<T> implements IStorage<T> {
4
+ constructor(private storage: IStorage<Buffer>) { }
5
+ public async get(key: string): Promise<T | undefined> {
6
+ let buffer = await this.storage.get(key);
7
+ if (buffer === undefined) {
8
+ return undefined;
9
+ }
10
+ try {
11
+ return JSON.parse(buffer.toString());
12
+ } catch (e) {
13
+ console.warn(`Failed to parse JSON for key: ${key}`, buffer.toString(), e);
14
+ }
15
+ }
16
+ public async set(key: string, value: T): Promise<void> {
17
+ await this.storage.set(key, Buffer.from(JSON.stringify(value)));
18
+ }
19
+ public async remove(key: string): Promise<void> {
20
+ await this.storage.remove(key);
21
+ }
22
+ public async getKeys(): Promise<string[]> {
23
+ return await this.storage.getKeys();
24
+ }
25
+ public async getInfo(key: string) {
26
+ return await this.storage.getInfo(key);
27
+ }
28
+
29
+
30
+
31
+
32
+ public async reset() {
33
+ await this.storage.reset();
34
+ }
35
+ }
@@ -0,0 +1,63 @@
1
+ import { throttleFunction } from "socket-function/src/misc";
2
+ import { observable } from "mobx";
3
+ import preact from "preact";
4
+ import { css } from "typesafecss";
5
+ import { observer } from "../web/observer";
6
+ import { formatTime } from "socket-function/src/formatting/format";
7
+
8
+ let watchState = observable({
9
+ pending: {} as { [group: string]: string }
10
+ });
11
+ let pendingLastSets = new Map<string, number>();
12
+
13
+ let pendingCache = new Map<string, string>();
14
+
15
+ // "" clears the pending value
16
+ export function setPending(group: string, message: string) {
17
+ pendingCache.set(group, message);
18
+ void setPendingBase();
19
+ }
20
+
21
+ export function hasPending(): boolean {
22
+ return Object.keys(watchState.pending).length > 0;
23
+ }
24
+
25
+ // NOTE: This not only prevents render overload, but also means any pending that are < this
26
+ // delay don't show up (which is useful to reduce unnecessary pending messages).
27
+ const setPendingBase = throttleFunction(500, function setPendingBase() {
28
+ for (let [group, message] of pendingCache) {
29
+
30
+ if (!message) {
31
+ let lastSet = pendingLastSets.get(group);
32
+ if (lastSet) {
33
+ let duration = Date.now() - lastSet;
34
+ if (duration > 500) {
35
+ console.log(`Finished slow task after ${formatTime(duration)}: ${JSON.stringify(group)}, last is ${JSON.stringify(watchState.pending[group])}`);
36
+ }
37
+ pendingLastSets.delete(group);
38
+ }
39
+ delete watchState.pending[group];
40
+ } else {
41
+ //console.log("setPending", group, message);
42
+ if (!(group in watchState.pending)) {
43
+ pendingLastSets.set(group, Date.now());
44
+ }
45
+ watchState.pending[group] = message;
46
+ }
47
+ }
48
+ pendingCache.clear();
49
+ });
50
+
51
+ @observer
52
+ export class PendingDisplay extends preact.Component {
53
+ render() {
54
+ // Single line, giving equal space, and ellipsis for overflow
55
+ return <div className={css.hbox(10)}>
56
+ {Object.keys(watchState.pending).map(group => (
57
+ <div className={css.center.textOverflow("ellipsis").border("1px solid black").pad2(6, 2)}>
58
+ {group}: {watchState.pending[group]}
59
+ </div>
60
+ ))}
61
+ </div>;
62
+ }
63
+ }
@@ -0,0 +1,47 @@
1
+ import { throttleFunction } from "socket-function/src/misc";
2
+ import { IStorage } from "./IStorage";
3
+ import { setPending } from "./PendingManager";
4
+
5
+ export class PendingStorage<T> implements IStorage<T> {
6
+ pending = new Map<string, number>();
7
+ constructor(
8
+ private pendingGroup: string,
9
+ private storage: IStorage<T>,
10
+ ) { }
11
+ public async get(key: string): Promise<T | undefined> {
12
+ return this.watchPending("get", this.storage.get(key));
13
+ }
14
+ public async set(key: string, value: T): Promise<void> {
15
+ return this.watchPending("set", this.storage.set(key, value));
16
+ }
17
+ public async remove(key: string): Promise<void> {
18
+ return this.watchPending("remove", this.storage.remove(key));
19
+ }
20
+ public async getKeys(): Promise<string[]> {
21
+ return this.watchPending("getKeys", this.storage.getKeys());
22
+ }
23
+ public async getInfo(key: string) {
24
+ return this.watchPending("getInfo", this.storage.getInfo(key));
25
+ }
26
+
27
+ private watchPending<T>(type: string, promise: Promise<T>): Promise<T> {
28
+ this.pending.set(type, (this.pending.get(type) || 0) + 1);
29
+ void this.updatePending();
30
+ void promise.finally(() => {
31
+ this.pending.set(type, (this.pending.get(type) || 0) - 1);
32
+ if (this.pending.get(type) === 0) {
33
+ this.pending.delete(type);
34
+ }
35
+ void this.updatePending();
36
+ });
37
+ return promise;
38
+ }
39
+ private updatePending = throttleFunction(100, () => {
40
+ let text = Array.from(this.pending.entries()).map(([key, value]) => `${key}: ${value}`).join(", ");
41
+ setPending(this.pendingGroup, text);
42
+ });
43
+
44
+ public async reset() {
45
+ return this.storage.reset();
46
+ }
47
+ }
@@ -0,0 +1,192 @@
1
+ import { IStorageRaw } from "./IStorage";
2
+
3
+ export class PrivateFileSystemStorage implements IStorageRaw {
4
+ private rootHandle: FileSystemDirectoryHandle | undefined;
5
+
6
+ constructor(private path: string) { }
7
+
8
+ private async ensureInitialized(): Promise<FileSystemDirectoryHandle> {
9
+ if (!this.rootHandle) {
10
+ if (!navigator.storage?.getDirectory) {
11
+ throw new Error("Private File System Access API not supported in this browser");
12
+ }
13
+ this.rootHandle = await navigator.storage.getDirectory();
14
+ }
15
+ return this.rootHandle;
16
+ }
17
+
18
+ private async directoryExists(): Promise<boolean> {
19
+ try {
20
+ const root = await this.ensureInitialized();
21
+
22
+ if (!this.path || this.path === "" || this.path === "/") {
23
+ return true; // Root always exists
24
+ }
25
+
26
+ const pathParts = this.path.split("/").filter(part => part.length > 0);
27
+ let currentHandle = root;
28
+
29
+ for (const part of pathParts) {
30
+ try {
31
+ currentHandle = await currentHandle.getDirectoryHandle(part);
32
+ } catch (error) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ return true;
38
+ } catch (error) {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ private async getDirectoryHandle(createPath: boolean = true): Promise<FileSystemDirectoryHandle> {
44
+ const root = await this.ensureInitialized();
45
+
46
+ if (!this.path || this.path === "" || this.path === "/") {
47
+ return root;
48
+ }
49
+
50
+ const pathParts = this.path.split("/").filter(part => part.length > 0);
51
+ let currentHandle = root;
52
+
53
+ for (const part of pathParts) {
54
+ try {
55
+ currentHandle = await currentHandle.getDirectoryHandle(part, { create: createPath });
56
+ } catch (error) {
57
+ if (!createPath) {
58
+ throw new Error(`Directory not found: ${this.path}`);
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ return currentHandle;
65
+ }
66
+
67
+ private async getFileHandle(key: string, create: boolean = false): Promise<FileSystemFileHandle | undefined> {
68
+ try {
69
+ const dirHandle = await this.getDirectoryHandle(create);
70
+ return await dirHandle.getFileHandle(key, { create });
71
+ } catch (error) {
72
+ return undefined;
73
+ }
74
+ }
75
+
76
+ private async fileExists(key: string): Promise<boolean> {
77
+ try {
78
+ // First check if directory exists
79
+ if (!(await this.directoryExists())) {
80
+ return false;
81
+ }
82
+
83
+ const dirHandle = await this.getDirectoryHandle(false);
84
+ await dirHandle.getFileHandle(key);
85
+ return true;
86
+ } catch (error) {
87
+ return false;
88
+ }
89
+ }
90
+
91
+ public async get(key: string): Promise<Buffer | undefined> {
92
+ // Check if file exists first to avoid unnecessary errors
93
+ if (!(await this.fileExists(key))) {
94
+ return undefined;
95
+ }
96
+
97
+ try {
98
+ const fileHandle = await this.getFileHandle(key, false);
99
+ if (!fileHandle) {
100
+ return undefined;
101
+ }
102
+
103
+ const file = await fileHandle.getFile();
104
+ const arrayBuffer = await file.arrayBuffer();
105
+ return Buffer.from(arrayBuffer);
106
+ } catch (error) {
107
+ console.warn(`Error reading file ${key}:`, error);
108
+ return undefined;
109
+ }
110
+ }
111
+
112
+ public async set(key: string, value: Buffer): Promise<void> {
113
+ try {
114
+ const fileHandle = await this.getFileHandle(key, true);
115
+ if (!fileHandle) {
116
+ throw new Error(`Failed to create file handle for key: ${key}`);
117
+ }
118
+
119
+ const writable = await fileHandle.createWritable();
120
+ await writable.write(value);
121
+ await writable.close();
122
+ } catch (error) {
123
+ throw new Error(`Failed to write file ${key}: ${error}`);
124
+ }
125
+ }
126
+
127
+ public async append(key: string, value: Buffer): Promise<void> {
128
+ const existingContent = await this.get(key);
129
+ let newContent: Buffer;
130
+ if (existingContent) {
131
+ newContent = Buffer.concat([existingContent, value]);
132
+ } else {
133
+ newContent = value;
134
+ }
135
+ await this.set(key, newContent);
136
+ }
137
+
138
+ public async remove(key: string): Promise<void> {
139
+ // Check if file exists first to avoid unnecessary errors
140
+ if (!(await this.fileExists(key))) {
141
+ return; // File doesn't exist, nothing to remove
142
+ }
143
+
144
+ try {
145
+ const dirHandle = await this.getDirectoryHandle(false);
146
+ await dirHandle.removeEntry(key);
147
+ } catch { }
148
+ }
149
+
150
+ public async getKeys(): Promise<string[]> {
151
+ // Check if directory exists first to avoid unnecessary errors
152
+ if (!(await this.directoryExists())) {
153
+ return [];
154
+ }
155
+
156
+ try {
157
+ const dirHandle = await this.getDirectoryHandle(false);
158
+ const keys: string[] = [];
159
+
160
+ // Use the async iterator protocol for FileSystemDirectoryHandle
161
+ for await (const [name, handle] of (dirHandle as any).entries()) {
162
+ if (handle.kind === "file") {
163
+ keys.push(name);
164
+ }
165
+ }
166
+
167
+ return keys.sort();
168
+ } catch (error) {
169
+ if (error instanceof Error && error.message.includes("Directory not found")) {
170
+ return [];
171
+ }
172
+ throw new Error(`Failed to list files: ${error}`);
173
+ }
174
+ }
175
+ public async getInfo(key: string): Promise<undefined | {
176
+ size: number;
177
+ lastModified: number;
178
+ }> {
179
+ let fileHandle = await this.getFileHandle(key, false);
180
+ if (!fileHandle) {
181
+ return undefined;
182
+ }
183
+ let file = await fileHandle.getFile();
184
+ return {
185
+ size: file.size,
186
+ lastModified: file.lastModified,
187
+ };
188
+ }
189
+ public async reset(): Promise<void> {
190
+ throw new Error("Not implemented");
191
+ }
192
+ }
@@ -0,0 +1,122 @@
1
+ import { observable } from "mobx";
2
+ import { isDefined } from "../misc/types";
3
+ import { IStorage, IStorageSync } from "./IStorage";
4
+
5
+ // NOTE: At around 500K values (depending on their size to some degree), this will take about 2 minutes to load. But once it does it will be fast. So... keep that in mind. I recommend not exceeding 100K
6
+ export class StorageSync<T> implements IStorageSync<T> {
7
+ cached = observable.map<string, T | undefined>(undefined, { deep: false });
8
+ infoCached = observable.map<string, { size: number; lastModified: number } | undefined>(undefined, { deep: false });
9
+ keys = new Set<string>();
10
+ synced = observable({
11
+ keySeqNum: 0,
12
+ }, undefined, { deep: false });
13
+
14
+ constructor(public storage: IStorage<T>) { }
15
+
16
+ public get(key: string): T | undefined {
17
+ if (!this.cached.has(key)) {
18
+ this.cached.set(key, undefined);
19
+ void this.getPromise(key);
20
+ }
21
+ if (this.cached.get(key) === undefined) {
22
+ this.synced.keySeqNum;
23
+ }
24
+ return this.cached.get(key);
25
+ }
26
+ public set(key: string, value: T): void {
27
+ if (!this.keys.has(key)) {
28
+ this.keys.add(key);
29
+ this.synced.keySeqNum++;
30
+ }
31
+ this.cached.set(key, value);
32
+ void this.storage.set(key, value);
33
+ }
34
+ public remove(key: string): void {
35
+ if (this.keys.has(key)) {
36
+ this.keys.delete(key);
37
+ this.synced.keySeqNum++;
38
+ }
39
+ this.cached.delete(key);
40
+ void this.storage.remove(key);
41
+ }
42
+ private loadedKeys = false;
43
+ public getKeys(): string[] {
44
+ void this.getKeysPromise();
45
+ this.synced.keySeqNum;
46
+ return Array.from(this.keys);
47
+ }
48
+
49
+ public getInfo(key: string): { size: number; lastModified: number } | undefined {
50
+ if (!this.infoCached.has(key)) {
51
+ this.infoCached.set(key, { size: 0, lastModified: 0 });
52
+ void this.storage.getInfo(key).then(info => {
53
+ this.infoCached.set(key, info);
54
+ });
55
+ }
56
+ return this.infoCached.get(key);
57
+ }
58
+
59
+ public getValues(): T[] {
60
+ let keys = this.getKeys();
61
+ return keys.map(key => this.get(key)).filter(isDefined);
62
+ }
63
+ public getEntries(): [string, T][] {
64
+ let keys = this.getKeys();
65
+ return keys.map(key => [key, this.get(key)]).filter(([_, value]) => isDefined(value)) as [string, T][];
66
+ }
67
+
68
+
69
+ public async getPromise(key: string): Promise<T | undefined> {
70
+ let value = this.cached.get(key);
71
+ if (value === undefined) {
72
+ value = await this.storage.get(key);
73
+ if (this.cached.get(key) === undefined) {
74
+ this.cached.set(key, value);
75
+ }
76
+ }
77
+ return value;
78
+ }
79
+ private pendingGetKeys: Promise<string[]> | undefined;
80
+ public async getKeysPromise(): Promise<string[]> {
81
+ if (this.pendingGetKeys) {
82
+ return this.pendingGetKeys;
83
+ }
84
+ if (this.loadedKeys) return Array.from(this.keys);
85
+ this.loadedKeys = true;
86
+ this.pendingGetKeys = this.storage.getKeys();
87
+ void this.pendingGetKeys.finally(() => {
88
+ this.pendingGetKeys = undefined;
89
+ });
90
+ let keys = await this.pendingGetKeys;
91
+ if (keys.length > 0) {
92
+ this.keys = new Set(keys);
93
+ this.synced.keySeqNum++;
94
+ }
95
+ return Array.from(this.keys);
96
+ }
97
+
98
+ public reload() {
99
+ this.loadedKeys = false;
100
+ this.synced.keySeqNum++;
101
+ this.cached.clear();
102
+ this.infoCached.clear();
103
+ this.keys.clear();
104
+ }
105
+ public reloadKeys() {
106
+ this.loadedKeys = false;
107
+ this.synced.keySeqNum++;
108
+ }
109
+ public reloadKey(key: string) {
110
+ this.cached.delete(key);
111
+ this.infoCached.delete(key);
112
+ this.keys.delete(key);
113
+ }
114
+
115
+ public async reset() {
116
+ this.cached.clear();
117
+ this.infoCached.clear();
118
+ this.keys.clear();
119
+ this.synced.keySeqNum++;
120
+ await this.storage.reset();
121
+ }
122
+ }