@vezham/hooks 26.1.0 → 26.3.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/README.md +47 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/lib/use-app/index.d.ts +10 -0
- package/dist/lib/use-app/index.d.ts.map +1 -0
- package/dist/lib/use-app/index.js +6 -0
- package/dist/lib/{use-command.d.ts → use-command/index.d.ts} +1 -1
- package/dist/lib/use-command/index.d.ts.map +1 -0
- package/dist/lib/{use-command.js → use-command/index.js} +1 -1
- package/dist/lib/use-copy-to-clipboard/index.d.ts +8 -0
- package/dist/lib/use-copy-to-clipboard/index.d.ts.map +1 -0
- package/dist/lib/use-copy-to-clipboard/index.js +23 -0
- package/dist/lib/use-copy-to-clipboard/write-clipboard-text.d.ts +2 -0
- package/dist/lib/use-copy-to-clipboard/write-clipboard-text.d.ts.map +1 -0
- package/dist/lib/use-copy-to-clipboard/write-clipboard-text.js +26 -0
- package/dist/lib/use-indexed-db-store/database.d.ts +6 -0
- package/dist/lib/use-indexed-db-store/database.d.ts.map +1 -0
- package/dist/lib/use-indexed-db-store/database.js +83 -0
- package/dist/lib/use-indexed-db-store/index.d.ts +13 -0
- package/dist/lib/use-indexed-db-store/index.d.ts.map +1 -0
- package/dist/lib/use-indexed-db-store/index.js +20 -0
- package/dist/lib/use-indexed-db-store/indexed-db-store.d.ts +23 -0
- package/dist/lib/use-indexed-db-store/indexed-db-store.d.ts.map +1 -0
- package/dist/lib/use-indexed-db-store/indexed-db-store.js +131 -0
- package/dist/lib/use-local-store/index.d.ts +12 -0
- package/dist/lib/use-local-store/index.d.ts.map +1 -0
- package/dist/lib/use-local-store/index.js +23 -0
- package/dist/lib/use-local-store/local-store.d.ts +20 -0
- package/dist/lib/use-local-store/local-store.d.ts.map +1 -0
- package/dist/lib/use-local-store/local-store.js +141 -0
- package/package.json +1 -1
- package/dist/lib/use-command.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1 +1,48 @@
|
|
|
1
1
|
# @vezham/hooks
|
|
2
|
+
|
|
3
|
+
## useLocalStore
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { useLocalStore } from '@vezham/hooks'
|
|
7
|
+
|
|
8
|
+
const DEFAULTS = { sounds: true }
|
|
9
|
+
const Preferences = () => {
|
|
10
|
+
const { value, setValue, reset, isReady, isSaved, error } = useLocalStore(
|
|
11
|
+
'my-app:preferences:v1',
|
|
12
|
+
DEFAULTS,
|
|
13
|
+
{ debounceMs: 150 }
|
|
14
|
+
)
|
|
15
|
+
// Use setValue(next) or setValue(current => next).
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Provide a stable default and an optional `parse(unknown)` function to validate
|
|
20
|
+
or migrate stored JSON. One key represents one schema. Reads start after mount,
|
|
21
|
+
so static/prerendered HTML has a deterministic initial value. Failed reads and
|
|
22
|
+
writes retain a usable in-memory value and expose `error`/`isSaved`. Defaults
|
|
23
|
+
are not saved during hydration; successful parser migrations are persisted.
|
|
24
|
+
|
|
25
|
+
Updates notify consumers in the current document and listen to `storage`
|
|
26
|
+
events from other tabs. Cross-tab conflicts use the latest storage event.
|
|
27
|
+
`reset()` removes only this key. Debounced writes flush on pagehide and final
|
|
28
|
+
unsubscribe; `flush()` can explicitly retry a failed write. Scope keys by host,
|
|
29
|
+
workspace, and user where needed. Do not store credentials.
|
|
30
|
+
|
|
31
|
+
## useCopyToClipboard
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { useCopyToClipboard } from '@vezham/hooks'
|
|
35
|
+
|
|
36
|
+
const CopyButton = ({ text }: { text: string }) => {
|
|
37
|
+
const { copy, isCopied, isUnavailable } = useCopyToClipboard()
|
|
38
|
+
return (
|
|
39
|
+
<button onClick={() => void copy(text)}>
|
|
40
|
+
{isCopied ? 'Copied' : isUnavailable ? 'Copy unavailable' : 'Copy'}
|
|
41
|
+
</button>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Uses the Clipboard API when available and falls back to a temporary selection for
|
|
47
|
+
restricted origins. The hook exposes the last copy result as `status`, `isCopied`,
|
|
48
|
+
and `isUnavailable`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
export { AppContext, useApp, type AppIdentity } from './lib/use-app';
|
|
1
2
|
export { CommandContext, useCommand } from './lib/use-command';
|
|
3
|
+
export { useLocalStore, type LocalStoreOptions } from './lib/use-local-store';
|
|
4
|
+
export { useCopyToClipboard, type CopyToClipboardStatus } from './lib/use-copy-to-clipboard';
|
|
5
|
+
export { useIndexedDBStore, type IndexedDBMutationOptions, type IndexedDBStoreOptions } from './lib/use-indexed-db-store';
|
|
2
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;AACpE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAC7E,OAAO,EACL,kBAAkB,EAClB,KAAK,qBAAqB,EAC3B,MAAM,6BAA6B,CAAA;AAEpC,OAAO,EACL,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC3B,MAAM,4BAA4B,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { AppContext, useApp } from "./lib/use-app/index.js";
|
|
2
|
+
import { CommandContext, useCommand } from "./lib/use-command/index.js";
|
|
3
|
+
import { useLocalStore } from "./lib/use-local-store/index.js";
|
|
4
|
+
import { useCopyToClipboard } from "./lib/use-copy-to-clipboard/index.js";
|
|
5
|
+
import { useIndexedDBStore } from "./lib/use-indexed-db-store/index.js";
|
|
6
|
+
export { AppContext, CommandContext, useApp, useCommand, useCopyToClipboard, useIndexedDBStore, useLocalStore };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface AppIdentity {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly version: string;
|
|
5
|
+
readonly environment: string;
|
|
6
|
+
readonly runtime: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const AppContext: import("react").Context<AppIdentity | undefined>;
|
|
9
|
+
export declare const useApp: () => AppIdentity | undefined;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/use-app/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED,eAAO,MAAM,UAAU,kDAAoD,CAAA;AAE3E,eAAO,MAAM,MAAM,+BAA+B,CAAA"}
|
|
@@ -7,4 +7,4 @@ interface CommandContextValue {
|
|
|
7
7
|
export declare const CommandContext: import("react").Context<CommandContextValue | null>;
|
|
8
8
|
export declare const useCommand: () => CommandContextValue;
|
|
9
9
|
export {};
|
|
10
|
-
//# sourceMappingURL=
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/use-command/index.ts"],"names":[],"mappings":"AAEA,UAAU,mBAAmB;IAC3B,MAAM,EAAE,OAAO,CAAA;IACf,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,aAAa,EAAE,MAAM,IAAI,CAAA;CAC1B;AAED,eAAO,MAAM,cAAc,qDAAkD,CAAA;AAE7E,eAAO,MAAM,UAAU,2BAQtB,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type CopyToClipboardStatus = 'idle' | 'copied' | 'unavailable';
|
|
2
|
+
export declare const useCopyToClipboard: () => {
|
|
3
|
+
copy: (text: string) => Promise<boolean>;
|
|
4
|
+
status: CopyToClipboardStatus;
|
|
5
|
+
isCopied: boolean;
|
|
6
|
+
isUnavailable: boolean;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/use-copy-to-clipboard/index.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,aAAa,CAAA;AAErE,eAAO,MAAM,kBAAkB;iBAES,MAAM;;;;CAiB7C,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { writeClipboardText } from "./write-clipboard-text.js";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
//#region src/lib/use-copy-to-clipboard/index.ts
|
|
4
|
+
var useCopyToClipboard = () => {
|
|
5
|
+
const [status, setStatus] = useState("idle");
|
|
6
|
+
return {
|
|
7
|
+
copy: useCallback(async (text) => {
|
|
8
|
+
try {
|
|
9
|
+
await writeClipboardText(text);
|
|
10
|
+
setStatus("copied");
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
setStatus("unavailable");
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}, []),
|
|
17
|
+
status,
|
|
18
|
+
isCopied: status === "copied",
|
|
19
|
+
isUnavailable: status === "unavailable"
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { useCopyToClipboard };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-clipboard-text.d.ts","sourceRoot":"","sources":["../../../src/lib/use-copy-to-clipboard/write-clipboard-text.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,GAAU,MAAM,MAAM,kBAkCpD,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/lib/use-copy-to-clipboard/write-clipboard-text.ts
|
|
2
|
+
var writeClipboardText = async (text) => {
|
|
3
|
+
if (typeof navigator !== "undefined" && typeof navigator.clipboard?.writeText === "function") try {
|
|
4
|
+
await navigator.clipboard.writeText(text);
|
|
5
|
+
return;
|
|
6
|
+
} catch {}
|
|
7
|
+
if (typeof document === "undefined" || !document.body) throw new Error("Clipboard copy is unavailable");
|
|
8
|
+
const input = document.createElement("textarea");
|
|
9
|
+
input.value = text;
|
|
10
|
+
input.setAttribute("readonly", "");
|
|
11
|
+
input.setAttribute("aria-hidden", "true");
|
|
12
|
+
input.tabIndex = -1;
|
|
13
|
+
input.style.position = "fixed";
|
|
14
|
+
input.style.top = "0";
|
|
15
|
+
input.style.left = "-9999px";
|
|
16
|
+
input.style.fontSize = "16px";
|
|
17
|
+
document.body.appendChild(input);
|
|
18
|
+
input.focus();
|
|
19
|
+
input.select();
|
|
20
|
+
input.setSelectionRange(0, text.length);
|
|
21
|
+
const copied = document.execCommand("copy");
|
|
22
|
+
input.remove();
|
|
23
|
+
if (!copied) throw new Error("Clipboard copy is unavailable");
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { writeClipboardText };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface IndexedDBMutationOptions {
|
|
2
|
+
optimistic?: boolean;
|
|
3
|
+
deletePrefixes?: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export declare const updateIndexedDBValue: <T>(factory: IDBFactory | undefined, key: string, initialValue: T, parse?: (value: unknown) => T, update?: (current: T) => T, options?: IndexedDBMutationOptions) => Promise<T>;
|
|
6
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/lib/use-indexed-db-store/database.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC;AA+BD,eAAO,MAAM,oBAAoB,GAAU,CAAC,EAC1C,SAAS,UAAU,GAAG,SAAS,EAC/B,KAAK,MAAM,EACX,cAAc,CAAC,EACf,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,EAC7B,SAAS,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,EAC1B,UAAS,wBAA6B,KACrC,OAAO,CAAC,CAAC,CAwEX,CAAA"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
//#region src/lib/use-indexed-db-store/database.ts
|
|
2
|
+
var INDEXED_DB_NAME = "vezham";
|
|
3
|
+
var RECORDS = "records";
|
|
4
|
+
var DELETED_PREFIXES = "\0deleted-prefixes";
|
|
5
|
+
var openDatabase = (factory) => new Promise((resolve, reject) => {
|
|
6
|
+
if (!factory) {
|
|
7
|
+
reject(/* @__PURE__ */ new Error("IndexedDB is unavailable in this browser."));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const request = factory.open(INDEXED_DB_NAME, 1);
|
|
11
|
+
let blocked = false;
|
|
12
|
+
request.onupgradeneeded = () => {
|
|
13
|
+
if (!request.result.objectStoreNames.contains(RECORDS)) request.result.createObjectStore(RECORDS);
|
|
14
|
+
};
|
|
15
|
+
request.onerror = () => reject(request.error ?? /* @__PURE__ */ new Error("Unable to open IndexedDB."));
|
|
16
|
+
request.onblocked = () => {
|
|
17
|
+
blocked = true;
|
|
18
|
+
reject(/* @__PURE__ */ new Error("IndexedDB is blocked by another open page."));
|
|
19
|
+
};
|
|
20
|
+
request.onsuccess = () => {
|
|
21
|
+
if (blocked) {
|
|
22
|
+
request.result.close();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
request.result.onversionchange = () => request.result.close();
|
|
26
|
+
resolve(request.result);
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
var updateIndexedDBValue = async (factory, key, initialValue, parse, update, options = {}) => {
|
|
30
|
+
const database = await openDatabase(factory);
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
let value;
|
|
33
|
+
let failure;
|
|
34
|
+
const transaction = database.transaction(RECORDS, "readwrite");
|
|
35
|
+
const records = transaction.objectStore(RECORDS);
|
|
36
|
+
const deletedRequest = records.get(DELETED_PREFIXES);
|
|
37
|
+
deletedRequest.onsuccess = () => {
|
|
38
|
+
const deleted = deletedRequest.result ?? [];
|
|
39
|
+
if (deleted.some((prefix) => key.startsWith(prefix))) {
|
|
40
|
+
failure = /* @__PURE__ */ new Error("This data scope has been deleted.");
|
|
41
|
+
transaction.abort();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const request = records.get(key);
|
|
45
|
+
request.onsuccess = () => {
|
|
46
|
+
try {
|
|
47
|
+
const stored = request.result;
|
|
48
|
+
const current = stored === void 0 ? initialValue : parse ? parse(stored) : stored;
|
|
49
|
+
value = update ? update(current) : current;
|
|
50
|
+
if (update || stored === void 0) records.put(value, key);
|
|
51
|
+
const prefixes = options.deletePrefixes ?? [];
|
|
52
|
+
if (prefixes.length) {
|
|
53
|
+
if (!update || prefixes.some((prefix) => !prefix || key.startsWith(prefix))) throw new Error("Invalid IndexedDB deletion scope.");
|
|
54
|
+
records.put([.../* @__PURE__ */ new Set([...deleted, ...prefixes])], DELETED_PREFIXES);
|
|
55
|
+
const cursor = records.openCursor();
|
|
56
|
+
cursor.onsuccess = () => {
|
|
57
|
+
const entry = cursor.result;
|
|
58
|
+
if (!entry) return;
|
|
59
|
+
if (typeof entry.key === "string" && prefixes.some((prefix) => entry.key.toString().startsWith(prefix))) entry.delete();
|
|
60
|
+
entry.continue();
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
} catch (cause) {
|
|
64
|
+
failure = cause;
|
|
65
|
+
transaction.abort();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
transaction.oncomplete = () => {
|
|
70
|
+
database.close();
|
|
71
|
+
resolve(value);
|
|
72
|
+
};
|
|
73
|
+
transaction.onabort = () => {
|
|
74
|
+
database.close();
|
|
75
|
+
reject(failure ?? transaction.error ?? /* @__PURE__ */ new Error("IndexedDB transaction was aborted."));
|
|
76
|
+
};
|
|
77
|
+
transaction.onerror = () => {
|
|
78
|
+
failure ??= transaction.error;
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
export { updateIndexedDBValue };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IndexedDBStoreOptions } from './indexed-db-store';
|
|
2
|
+
export declare const useIndexedDBStore: <T>(key: string, initialValue: T, options?: IndexedDBStoreOptions<T>) => {
|
|
3
|
+
setValue: (update: T | ((current: T) => T), options?: import("./database").IndexedDBMutationOptions) => Promise<boolean>;
|
|
4
|
+
flush: () => Promise<undefined>;
|
|
5
|
+
refresh: () => Promise<undefined>;
|
|
6
|
+
value: T;
|
|
7
|
+
isReady: boolean;
|
|
8
|
+
isSaved: boolean;
|
|
9
|
+
error: unknown;
|
|
10
|
+
};
|
|
11
|
+
export type { IndexedDBStoreOptions } from './indexed-db-store';
|
|
12
|
+
export type { IndexedDBMutationOptions } from './database';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/use-indexed-db-store/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAI/D,eAAO,MAAM,iBAAiB,GAAI,CAAC,EACjC,KAAK,MAAM,EACX,cAAc,CAAC,EACf,UAAS,qBAAqB,CAAC,CAAC,CAAM;;;;;;;;CAkBvC,CAAA;AAED,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAE/D,YAAY,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getIndexedDBStore } from "./indexed-db-store.js";
|
|
2
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
3
|
+
//#region src/lib/use-indexed-db-store/index.ts
|
|
4
|
+
var disabledSubscribe = () => () => void 0;
|
|
5
|
+
var useIndexedDBStore = (key, initialValue, options = {}) => {
|
|
6
|
+
const { parse, enabled = true } = options;
|
|
7
|
+
const store = useMemo(() => getIndexedDBStore(key, initialValue, parse), [
|
|
8
|
+
key,
|
|
9
|
+
initialValue,
|
|
10
|
+
parse
|
|
11
|
+
]);
|
|
12
|
+
return {
|
|
13
|
+
...useSyncExternalStore(enabled ? store.subscribe : disabledSubscribe, enabled ? store.getSnapshot : store.getInitialSnapshot, store.getInitialSnapshot),
|
|
14
|
+
setValue: store.setValue,
|
|
15
|
+
flush: store.flush,
|
|
16
|
+
refresh: store.refresh
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
export { useIndexedDBStore };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type IndexedDBMutationOptions } from './database';
|
|
2
|
+
export interface IndexedDBStoreOptions<T> {
|
|
3
|
+
parse?: (value: unknown) => T;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface Snapshot<T> {
|
|
7
|
+
value: T;
|
|
8
|
+
isReady: boolean;
|
|
9
|
+
isSaved: boolean;
|
|
10
|
+
error: unknown;
|
|
11
|
+
}
|
|
12
|
+
declare const createIndexedDBStore: <T>(key: string, initialValue: T, parse?: (value: unknown) => T) => {
|
|
13
|
+
getSnapshot: () => Snapshot<T>;
|
|
14
|
+
getInitialSnapshot: () => Snapshot<T>;
|
|
15
|
+
setValue: (update: T | ((current: T) => T), options?: IndexedDBMutationOptions) => Promise<boolean>;
|
|
16
|
+
refresh: () => Promise<undefined>;
|
|
17
|
+
flush: () => Promise<undefined>;
|
|
18
|
+
subscribe: (listener: () => void) => () => void;
|
|
19
|
+
};
|
|
20
|
+
type IndexedDBStore<T> = ReturnType<typeof createIndexedDBStore<T>>;
|
|
21
|
+
export declare const getIndexedDBStore: <T>(key: string, initialValue: T, parse?: (value: unknown) => T) => IndexedDBStore<T>;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=indexed-db-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexed-db-store.d.ts","sourceRoot":"","sources":["../../../src/lib/use-indexed-db-store/indexed-db-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,wBAAwB,EAAwB,MAAM,YAAY,CAAA;AAEhF,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,CAAA;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,UAAU,QAAQ,CAAC,CAAC;IAClB,KAAK,EAAE,CAAC,CAAA;IACR,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAED,QAAA,MAAM,oBAAoB,GAAI,CAAC,EAC7B,KAAK,MAAM,EACX,cAAc,CAAC,EACf,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;;;uBA4CnB,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,YACrB,wBAAwB,KACjC,OAAO,CAAC,OAAO,CAAC;;;0BA2DK,MAAM,IAAI;CA6BnC,CAAA;AAED,KAAK,cAAc,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAA;AAGnE,eAAO,MAAM,iBAAiB,GAAI,CAAC,EACjC,KAAK,MAAM,EACX,cAAc,CAAC,EACf,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,KAC5B,cAAc,CAAC,CAAC,CAE6B,CAAA"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { updateIndexedDBValue } from "./database.js";
|
|
2
|
+
//#region src/lib/use-indexed-db-store/indexed-db-store.ts
|
|
3
|
+
var createIndexedDBStore = (key, initialValue, parse) => {
|
|
4
|
+
const initial = {
|
|
5
|
+
value: initialValue,
|
|
6
|
+
isReady: false,
|
|
7
|
+
isSaved: true,
|
|
8
|
+
error: null
|
|
9
|
+
};
|
|
10
|
+
let snapshot = initial;
|
|
11
|
+
let factory;
|
|
12
|
+
let channel;
|
|
13
|
+
let queue = Promise.resolve();
|
|
14
|
+
let revision = 0;
|
|
15
|
+
let pending = 0;
|
|
16
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
17
|
+
const emit = () => listeners.forEach((listener) => listener());
|
|
18
|
+
const fail = (error) => {
|
|
19
|
+
snapshot = {
|
|
20
|
+
...snapshot,
|
|
21
|
+
isReady: true,
|
|
22
|
+
isSaved: false,
|
|
23
|
+
error
|
|
24
|
+
};
|
|
25
|
+
emit();
|
|
26
|
+
};
|
|
27
|
+
const acceptRead = (value, version) => {
|
|
28
|
+
if (revision !== version || pending) return;
|
|
29
|
+
snapshot = {
|
|
30
|
+
value,
|
|
31
|
+
isReady: true,
|
|
32
|
+
isSaved: true,
|
|
33
|
+
error: null
|
|
34
|
+
};
|
|
35
|
+
emit();
|
|
36
|
+
};
|
|
37
|
+
const read = async () => {
|
|
38
|
+
const version = revision;
|
|
39
|
+
try {
|
|
40
|
+
const value = await updateIndexedDBValue(factory, key, initialValue, parse);
|
|
41
|
+
acceptRead(value, version);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (revision === version) fail(error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const refresh = () => {
|
|
47
|
+
queue = queue.then(read);
|
|
48
|
+
return queue.then(() => void 0);
|
|
49
|
+
};
|
|
50
|
+
const setValue = (update, options) => {
|
|
51
|
+
if (!snapshot.isReady || snapshot.error) return Promise.resolve(false);
|
|
52
|
+
const apply = (current) => typeof update === "function" ? update(current) : update;
|
|
53
|
+
let optimistic;
|
|
54
|
+
try {
|
|
55
|
+
optimistic = apply(snapshot.value);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
fail(error);
|
|
58
|
+
return Promise.resolve(false);
|
|
59
|
+
}
|
|
60
|
+
const version = ++revision;
|
|
61
|
+
pending++;
|
|
62
|
+
snapshot = {
|
|
63
|
+
value: options?.optimistic === false ? snapshot.value : optimistic,
|
|
64
|
+
isReady: true,
|
|
65
|
+
isSaved: false,
|
|
66
|
+
error: null
|
|
67
|
+
};
|
|
68
|
+
emit();
|
|
69
|
+
const operation = queue.then(async () => {
|
|
70
|
+
try {
|
|
71
|
+
const value = await updateIndexedDBValue(factory, key, initialValue, parse, apply, options);
|
|
72
|
+
pending--;
|
|
73
|
+
if (version === revision) {
|
|
74
|
+
snapshot = {
|
|
75
|
+
value,
|
|
76
|
+
isReady: true,
|
|
77
|
+
isSaved: pending === 0,
|
|
78
|
+
error: null
|
|
79
|
+
};
|
|
80
|
+
emit();
|
|
81
|
+
}
|
|
82
|
+
channel?.postMessage(key);
|
|
83
|
+
return true;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
pending--;
|
|
86
|
+
if (version === revision) fail(error);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
queue = operation.then(() => void 0);
|
|
91
|
+
return operation;
|
|
92
|
+
};
|
|
93
|
+
const store = {
|
|
94
|
+
getSnapshot: () => snapshot,
|
|
95
|
+
getInitialSnapshot: () => initial,
|
|
96
|
+
setValue,
|
|
97
|
+
refresh,
|
|
98
|
+
flush: () => queue.then(() => void 0),
|
|
99
|
+
subscribe: (listener) => {
|
|
100
|
+
const first = listeners.size === 0;
|
|
101
|
+
listeners.add(listener);
|
|
102
|
+
if (first) {
|
|
103
|
+
stores.set(key, store);
|
|
104
|
+
factory ??= globalThis.indexedDB;
|
|
105
|
+
if (!channel && typeof BroadcastChannel !== "undefined") {
|
|
106
|
+
channel = new BroadcastChannel("vezham:indexeddb");
|
|
107
|
+
channel.onmessage = (event) => {
|
|
108
|
+
if (event.data === key && listeners.size) refresh();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
refresh();
|
|
112
|
+
}
|
|
113
|
+
return () => {
|
|
114
|
+
listeners.delete(listener);
|
|
115
|
+
if (listeners.size) return;
|
|
116
|
+
if (stores.get(key) === store) stores.delete(key);
|
|
117
|
+
queue.then(() => {
|
|
118
|
+
if (!listeners.size) {
|
|
119
|
+
channel?.close();
|
|
120
|
+
channel = void 0;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
return store;
|
|
127
|
+
};
|
|
128
|
+
var stores = /* @__PURE__ */ new Map();
|
|
129
|
+
var getIndexedDBStore = (key, initialValue, parse) => stores.get(key) ?? createIndexedDBStore(key, initialValue, parse);
|
|
130
|
+
//#endregion
|
|
131
|
+
export { getIndexedDBStore };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LocalStoreOptions } from './local-store';
|
|
2
|
+
export declare const useLocalStore: <T>(key: string, initialValue: T, options?: LocalStoreOptions<T>) => {
|
|
3
|
+
setValue: (update: T | ((current: T) => T)) => void;
|
|
4
|
+
reset: () => void;
|
|
5
|
+
flush: () => void;
|
|
6
|
+
value: T;
|
|
7
|
+
isReady: boolean;
|
|
8
|
+
isSaved: boolean;
|
|
9
|
+
error: unknown;
|
|
10
|
+
};
|
|
11
|
+
export type { LocalStoreOptions } from './local-store';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/use-local-store/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAItD,eAAO,MAAM,aAAa,GAAI,CAAC,EAC7B,KAAK,MAAM,EACX,cAAc,CAAC,EACf,UAAS,iBAAiB,CAAC,CAAC,CAAM;;;;;;;;CAkBnC,CAAA;AAED,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getLocalStore } from "./local-store.js";
|
|
2
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
3
|
+
//#region src/lib/use-local-store/index.ts
|
|
4
|
+
var useLocalStore = (key, initialValue, options = {}) => {
|
|
5
|
+
const { parse, debounceMs } = options;
|
|
6
|
+
const store = useMemo(() => getLocalStore(key, initialValue, {
|
|
7
|
+
parse,
|
|
8
|
+
debounceMs
|
|
9
|
+
}), [
|
|
10
|
+
key,
|
|
11
|
+
initialValue,
|
|
12
|
+
parse,
|
|
13
|
+
debounceMs
|
|
14
|
+
]);
|
|
15
|
+
return {
|
|
16
|
+
...useSyncExternalStore(store.subscribe, store.getSnapshot, store.getInitialSnapshot),
|
|
17
|
+
setValue: store.setValue,
|
|
18
|
+
reset: store.reset,
|
|
19
|
+
flush: store.flush
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { useLocalStore };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface LocalStoreOptions<T> {
|
|
2
|
+
parse?: (value: unknown) => T;
|
|
3
|
+
debounceMs?: number;
|
|
4
|
+
}
|
|
5
|
+
interface Snapshot<T> {
|
|
6
|
+
value: T;
|
|
7
|
+
isReady: boolean;
|
|
8
|
+
isSaved: boolean;
|
|
9
|
+
error: unknown;
|
|
10
|
+
}
|
|
11
|
+
export declare const getLocalStore: <T>(key: string, initialValue: T, options: LocalStoreOptions<T>) => {
|
|
12
|
+
getSnapshot: () => Snapshot<T>;
|
|
13
|
+
getInitialSnapshot: () => Snapshot<T>;
|
|
14
|
+
setValue: (update: T | ((current: T) => T)) => void;
|
|
15
|
+
reset: () => void;
|
|
16
|
+
flush: () => void;
|
|
17
|
+
subscribe: (listener: () => void) => () => void;
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=local-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-store.d.ts","sourceRoot":"","sources":["../../../src/lib/use-local-store/local-store.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,CAAA;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,UAAU,QAAQ,CAAC,CAAC;IAClB,KAAK,EAAE,CAAC,CAAA;IACR,OAAO,EAAE,OAAO,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,OAAO,CAAA;CACf;AAsHD,eAAO,MAAM,aAAa,GAAI,CAAC,EAC7B,KAAK,MAAM,EACX,cAAc,CAAC,EACf,SAAS,iBAAiB,CAAC,CAAC,CAAC;;;;;;gCA5BC,IAAI;CAmCnC,CAAA"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
//#region src/lib/use-local-store/local-store.ts
|
|
2
|
+
var createLocalStore = (key, initialValue, { parse, debounceMs = 0 }) => {
|
|
3
|
+
const initial = {
|
|
4
|
+
value: initialValue,
|
|
5
|
+
isReady: false,
|
|
6
|
+
isSaved: true,
|
|
7
|
+
error: null
|
|
8
|
+
};
|
|
9
|
+
let snapshot = initial;
|
|
10
|
+
let dirty = false;
|
|
11
|
+
let timer;
|
|
12
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
13
|
+
const emit = () => listeners.forEach((listener) => listener());
|
|
14
|
+
const cancel = () => {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
timer = void 0;
|
|
17
|
+
};
|
|
18
|
+
const flush = () => {
|
|
19
|
+
cancel();
|
|
20
|
+
if (!dirty || typeof window === "undefined") return;
|
|
21
|
+
try {
|
|
22
|
+
window.localStorage.setItem(key, JSON.stringify(snapshot.value));
|
|
23
|
+
dirty = false;
|
|
24
|
+
snapshot = {
|
|
25
|
+
...snapshot,
|
|
26
|
+
isSaved: true,
|
|
27
|
+
error: null
|
|
28
|
+
};
|
|
29
|
+
} catch (error) {
|
|
30
|
+
snapshot = {
|
|
31
|
+
...snapshot,
|
|
32
|
+
isSaved: false,
|
|
33
|
+
error
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
emit();
|
|
37
|
+
};
|
|
38
|
+
const read = () => {
|
|
39
|
+
try {
|
|
40
|
+
const raw = window.localStorage.getItem(key);
|
|
41
|
+
const decoded = raw === null ? initialValue : JSON.parse(raw);
|
|
42
|
+
const value = raw === null ? initialValue : parse ? parse(decoded) : decoded;
|
|
43
|
+
snapshot = {
|
|
44
|
+
value,
|
|
45
|
+
isReady: true,
|
|
46
|
+
isSaved: true,
|
|
47
|
+
error: null
|
|
48
|
+
};
|
|
49
|
+
if (raw !== null && parse && JSON.stringify(value) !== raw) {
|
|
50
|
+
dirty = true;
|
|
51
|
+
flush();
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
snapshot = {
|
|
55
|
+
value: initialValue,
|
|
56
|
+
isReady: true,
|
|
57
|
+
isSaved: false,
|
|
58
|
+
error
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
emit();
|
|
62
|
+
};
|
|
63
|
+
const onStorage = (event) => {
|
|
64
|
+
if (event.key !== null && event.key !== key) return;
|
|
65
|
+
if (event.storageArea && event.storageArea !== window.localStorage) return;
|
|
66
|
+
cancel();
|
|
67
|
+
dirty = false;
|
|
68
|
+
read();
|
|
69
|
+
};
|
|
70
|
+
const setValue = (update) => {
|
|
71
|
+
if (typeof window === "undefined") return;
|
|
72
|
+
if (!snapshot.isReady) read();
|
|
73
|
+
snapshot = {
|
|
74
|
+
value: typeof update === "function" ? update(snapshot.value) : update,
|
|
75
|
+
isReady: true,
|
|
76
|
+
isSaved: false,
|
|
77
|
+
error: null
|
|
78
|
+
};
|
|
79
|
+
dirty = true;
|
|
80
|
+
emit();
|
|
81
|
+
cancel();
|
|
82
|
+
if (debounceMs > 0) timer = setTimeout(flush, debounceMs);
|
|
83
|
+
else flush();
|
|
84
|
+
};
|
|
85
|
+
const reset = () => {
|
|
86
|
+
cancel();
|
|
87
|
+
dirty = false;
|
|
88
|
+
snapshot = {
|
|
89
|
+
value: initialValue,
|
|
90
|
+
isReady: true,
|
|
91
|
+
isSaved: true,
|
|
92
|
+
error: null
|
|
93
|
+
};
|
|
94
|
+
try {
|
|
95
|
+
window.localStorage.removeItem(key);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
snapshot = {
|
|
98
|
+
...snapshot,
|
|
99
|
+
isSaved: false,
|
|
100
|
+
error
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
emit();
|
|
104
|
+
};
|
|
105
|
+
const store = {
|
|
106
|
+
getSnapshot: () => snapshot,
|
|
107
|
+
getInitialSnapshot: () => initial,
|
|
108
|
+
setValue,
|
|
109
|
+
reset,
|
|
110
|
+
flush,
|
|
111
|
+
subscribe: (listener) => {
|
|
112
|
+
const first = listeners.size === 0;
|
|
113
|
+
listeners.add(listener);
|
|
114
|
+
if (first) {
|
|
115
|
+
stores.set(key, store);
|
|
116
|
+
if (!dirty) read();
|
|
117
|
+
window.addEventListener("storage", onStorage);
|
|
118
|
+
window.addEventListener("pagehide", flush);
|
|
119
|
+
}
|
|
120
|
+
return () => {
|
|
121
|
+
listeners.delete(listener);
|
|
122
|
+
if (listeners.size) return;
|
|
123
|
+
flush();
|
|
124
|
+
window.removeEventListener("storage", onStorage);
|
|
125
|
+
window.removeEventListener("pagehide", flush);
|
|
126
|
+
if (stores.get(key) === store) stores.delete(key);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
return store;
|
|
131
|
+
};
|
|
132
|
+
var stores = /* @__PURE__ */ new Map();
|
|
133
|
+
var getLocalStore = (key, initialValue, options) => {
|
|
134
|
+
const existing = stores.get(key);
|
|
135
|
+
if (existing) return existing;
|
|
136
|
+
const store = createLocalStore(key, initialValue, options);
|
|
137
|
+
stores.set(key, store);
|
|
138
|
+
return store;
|
|
139
|
+
};
|
|
140
|
+
//#endregion
|
|
141
|
+
export { getLocalStore };
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"use-command.d.ts","sourceRoot":"","sources":["../../src/lib/use-command.ts"],"names":[],"mappings":"AAEA,UAAU,mBAAmB;IAC3B,MAAM,EAAE,OAAO,CAAA;IACf,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,YAAY,EAAE,MAAM,IAAI,CAAA;IACxB,aAAa,EAAE,MAAM,IAAI,CAAA;CAC1B;AAED,eAAO,MAAM,cAAc,qDAAkD,CAAA;AAE7E,eAAO,MAAM,UAAU,2BAQtB,CAAA"}
|