@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.
Files changed (33) hide show
  1. package/README.md +47 -0
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -2
  5. package/dist/lib/use-app/index.d.ts +10 -0
  6. package/dist/lib/use-app/index.d.ts.map +1 -0
  7. package/dist/lib/use-app/index.js +6 -0
  8. package/dist/lib/{use-command.d.ts → use-command/index.d.ts} +1 -1
  9. package/dist/lib/use-command/index.d.ts.map +1 -0
  10. package/dist/lib/{use-command.js → use-command/index.js} +1 -1
  11. package/dist/lib/use-copy-to-clipboard/index.d.ts +8 -0
  12. package/dist/lib/use-copy-to-clipboard/index.d.ts.map +1 -0
  13. package/dist/lib/use-copy-to-clipboard/index.js +23 -0
  14. package/dist/lib/use-copy-to-clipboard/write-clipboard-text.d.ts +2 -0
  15. package/dist/lib/use-copy-to-clipboard/write-clipboard-text.d.ts.map +1 -0
  16. package/dist/lib/use-copy-to-clipboard/write-clipboard-text.js +26 -0
  17. package/dist/lib/use-indexed-db-store/database.d.ts +6 -0
  18. package/dist/lib/use-indexed-db-store/database.d.ts.map +1 -0
  19. package/dist/lib/use-indexed-db-store/database.js +83 -0
  20. package/dist/lib/use-indexed-db-store/index.d.ts +13 -0
  21. package/dist/lib/use-indexed-db-store/index.d.ts.map +1 -0
  22. package/dist/lib/use-indexed-db-store/index.js +20 -0
  23. package/dist/lib/use-indexed-db-store/indexed-db-store.d.ts +23 -0
  24. package/dist/lib/use-indexed-db-store/indexed-db-store.d.ts.map +1 -0
  25. package/dist/lib/use-indexed-db-store/indexed-db-store.js +131 -0
  26. package/dist/lib/use-local-store/index.d.ts +12 -0
  27. package/dist/lib/use-local-store/index.d.ts.map +1 -0
  28. package/dist/lib/use-local-store/index.js +23 -0
  29. package/dist/lib/use-local-store/local-store.d.ts +20 -0
  30. package/dist/lib/use-local-store/local-store.d.ts.map +1 -0
  31. package/dist/lib/use-local-store/local-store.js +141 -0
  32. package/package.json +1 -1
  33. 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
@@ -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 { CommandContext, useCommand } from "./lib/use-command.js";
2
- export { CommandContext, useCommand };
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"}
@@ -0,0 +1,6 @@
1
+ import { createContext, useContext } from "react";
2
+ //#region src/lib/use-app/index.ts
3
+ var AppContext = createContext(void 0);
4
+ var useApp = () => useContext(AppContext);
5
+ //#endregion
6
+ export { AppContext, useApp };
@@ -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=use-command.d.ts.map
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"}
@@ -1,5 +1,5 @@
1
1
  import { createContext, useContext } from "react";
2
- //#region src/lib/use-command.ts
2
+ //#region src/lib/use-command/index.ts
3
3
  var CommandContext = createContext(null);
4
4
  var useCommand = () => {
5
5
  const context = useContext(CommandContext);
@@ -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,2 @@
1
+ export declare const writeClipboardText: (text: string) => Promise<void>;
2
+ //# sourceMappingURL=write-clipboard-text.d.ts.map
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@vezham/hooks",
3
- "version": "26.1.0",
3
+ "version": "26.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/vezham/vx-fe-pkgs.git",
@@ -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"}