@bb-labs/local-db 0.0.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.
- package/README.md +179 -0
- package/dist/app/components/broadcast.d.ts +4 -0
- package/dist/app/components/broadcast.js +28 -0
- package/dist/app/components/idb.d.ts +12 -0
- package/dist/app/components/idb.js +45 -0
- package/dist/app/components/store-registry.d.ts +17 -0
- package/dist/app/components/store-registry.js +87 -0
- package/dist/app/use-local-db.d.ts +15 -0
- package/dist/app/use-local-db.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
## Introduction
|
|
2
|
+
|
|
3
|
+
A tiny **client-side React hook** for **persistent state**, backed by **IndexedDB**, with:
|
|
4
|
+
|
|
5
|
+
- ✅ Zod-based schema validation
|
|
6
|
+
- ✅ Cross-tab synchronization (BroadcastChannel)
|
|
7
|
+
- ✅ Repair of invalid persisted data
|
|
8
|
+
- ✅ SSR-safe hydration (`value: undefined` while loading)
|
|
9
|
+
- ✅ No external state library required
|
|
10
|
+
- ✅ Per-hook database & store targeting
|
|
11
|
+
- ✅ Deduplicated shared store per key (`useSyncExternalStore`)
|
|
12
|
+
|
|
13
|
+
Perfect for saving **user settings, UI state, drafts, local preferences**, or anything that should persist across reloads and sync across tabs.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @bigbang-sdk/local-db
|
|
21
|
+
# or
|
|
22
|
+
yarn add @bigbang-sdk/local-db
|
|
23
|
+
# or
|
|
24
|
+
pnpm add @bigbang-sdk/local-db
|
|
25
|
+
# or
|
|
26
|
+
bun add @bigbang-sdk/local-db
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires React 18+.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Quick Example
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { useLocalDb } from "@bigbang-sdk/local-db";
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
|
|
39
|
+
const SettingsSchema = z.object({
|
|
40
|
+
fontSize: z.number().min(10).max(32),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export default function SettingsPanel() {
|
|
44
|
+
const { value, setValue } = useLocalDb({
|
|
45
|
+
key: "settings",
|
|
46
|
+
schema: SettingsSchema,
|
|
47
|
+
initialValue: { fontSize: 16 },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (value === undefined) return <div>Loading…</div>; // hydrating
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div>
|
|
54
|
+
<div>Font size: {value.fontSize}px</div>
|
|
55
|
+
<button onClick={() => setValue({ fontSize: value.fontSize + 1 })}>Increase</button>
|
|
56
|
+
<button onClick={() => setValue({ fontSize: value.fontSize - 1 })}>Decrease</button>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
### `useLocalDb(options)`
|
|
67
|
+
|
|
68
|
+
| Option | Type | Default | Description |
|
|
69
|
+
| -------------- | ---------------- | ------------ | --------------------------------------------------------------------------- |
|
|
70
|
+
| `key` | `string` | **required** | The logical key used in IndexedDB & cross-tab sync. |
|
|
71
|
+
| `schema` | `z.ZodSchema<T>` | **required** | Validates and repairs stored values. |
|
|
72
|
+
| `initialValue` | `T \| null` | `null` | Used if no data exists or persisted data is invalid. Captured on first use. |
|
|
73
|
+
| `dbName` | `string` | `"local-db"` | Optional database name override. |
|
|
74
|
+
| `storeName` | `string` | `"local-db"` | Optional object store name override. |
|
|
75
|
+
|
|
76
|
+
### Returns
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
{
|
|
80
|
+
value: T | null | undefined;
|
|
81
|
+
setValue(next: T | null): void;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- **`value === undefined`** → still hydrating from IndexedDB
|
|
86
|
+
- **`value === null`** → no stored value
|
|
87
|
+
- **`value === T`** → validated, ready to use
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Cross-Tab Sync
|
|
92
|
+
|
|
93
|
+
Updates propagate instantly across open browser tabs/windows:
|
|
94
|
+
|
|
95
|
+
- Automatically via **BroadcastChannel**
|
|
96
|
+
- Repairs apply consistently across all tabs
|
|
97
|
+
- No unnecessary re-renders (deep-equality guarded)
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Validation & Repair
|
|
102
|
+
|
|
103
|
+
Every read and write is validated using your Zod schema.
|
|
104
|
+
|
|
105
|
+
If data is corrupted or has an outdated shape:
|
|
106
|
+
|
|
107
|
+
```diff
|
|
108
|
+
Persisted Data → schema.safeParse() → ✅ valid → used
|
|
109
|
+
Persisted Data → schema.safeParse() → ❌ invalid → repaired to initialValue
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The repaired value is **automatically written back** to IndexedDB.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## SSR Behavior (Next.js Friendly)
|
|
117
|
+
|
|
118
|
+
This hook is designed for **client components**.
|
|
119
|
+
|
|
120
|
+
- It does **not** access `window`, `indexedDB` or `BroadcastChannel` on the server.
|
|
121
|
+
- `value` begins as **`undefined`** so server and client HTML always match.
|
|
122
|
+
- Hydration + IDB read happens **only in the browser**.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Using Multiple Databases / Stores
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const { value, setValue } = useLocalDb({
|
|
130
|
+
key: "draft",
|
|
131
|
+
schema: DraftSchema,
|
|
132
|
+
initialValue: null,
|
|
133
|
+
dbName: "editor-db",
|
|
134
|
+
storeName: "drafts",
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Tabs will **only sync if both `dbName` and `storeName` match**.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Performance
|
|
143
|
+
|
|
144
|
+
- No-op writes are skipped using deep-equality.
|
|
145
|
+
- Broadcasts are only emitted on real changes.
|
|
146
|
+
- Hydration is lazy-per-key with `useSyncExternalStore`.
|
|
147
|
+
- UI remains responsive even if IndexedDB is slow.
|
|
148
|
+
|
|
149
|
+
### If you expect _high-frequency updates_ (e.g., live typing):
|
|
150
|
+
|
|
151
|
+
Consider batching or debouncing writes:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
setValue(next);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Why not localStorage?
|
|
160
|
+
|
|
161
|
+
| Feature | localStorage | @bigbang-sdk/local-db |
|
|
162
|
+
| ------------------------ | ------------------ | --------------------------------- |
|
|
163
|
+
| Large data | ❌ slow / capped | ✅ stores MBs efficiently |
|
|
164
|
+
| Cross-tab sync | ⚠️ yes, but coarse | ✅ fine-grained, structured clone |
|
|
165
|
+
| Validation | ❌ no | ✅ Zod |
|
|
166
|
+
| React state sync | ❌ manual | ✅ automatic |
|
|
167
|
+
| Safe corruption handling | ❌ none | ✅ self-repair |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Contributing
|
|
178
|
+
|
|
179
|
+
PRs welcome!
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DEFAULT_DB, DEFAULT_STORE } from "./idb";
|
|
2
|
+
const bcCache = new Map();
|
|
3
|
+
function nsKey(dbName, storeName) {
|
|
4
|
+
return `${dbName}/${storeName}`;
|
|
5
|
+
}
|
|
6
|
+
function channelName(dbName, storeName) {
|
|
7
|
+
// Namespaced channel; safe across multiple store pairs
|
|
8
|
+
return `local-db:${dbName}:${storeName}`;
|
|
9
|
+
}
|
|
10
|
+
export function getBroadcastChannel(opts) {
|
|
11
|
+
if (typeof window === "undefined")
|
|
12
|
+
return null;
|
|
13
|
+
const db = opts?.dbName ?? DEFAULT_DB;
|
|
14
|
+
const st = opts?.storeName ?? DEFAULT_STORE;
|
|
15
|
+
const k = nsKey(db, st);
|
|
16
|
+
const cached = bcCache.get(k);
|
|
17
|
+
if (cached !== undefined)
|
|
18
|
+
return cached;
|
|
19
|
+
try {
|
|
20
|
+
const bc = new BroadcastChannel(channelName(db, st));
|
|
21
|
+
bcCache.set(k, bc);
|
|
22
|
+
return bc;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
bcCache.set(k, null);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type UseStore } from "idb-keyval";
|
|
2
|
+
export declare const DEFAULT_DB = "local-db";
|
|
3
|
+
export declare const DEFAULT_STORE = "local-db";
|
|
4
|
+
export declare function getOrCreateIdbStore(dbName?: string, storeName?: string): UseStore | null;
|
|
5
|
+
export declare function idbGet<T>(key: string, opts?: {
|
|
6
|
+
dbName?: string;
|
|
7
|
+
storeName?: string;
|
|
8
|
+
}): Promise<T | undefined>;
|
|
9
|
+
export declare function idbSet<T>(key: string, value: T | null, opts?: {
|
|
10
|
+
dbName?: string;
|
|
11
|
+
storeName?: string;
|
|
12
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createStore, del, get, set } from "idb-keyval";
|
|
2
|
+
export const DEFAULT_DB = "local-db";
|
|
3
|
+
export const DEFAULT_STORE = "local-db";
|
|
4
|
+
const storeCache = new Map();
|
|
5
|
+
function keyFor(dbName, storeName) {
|
|
6
|
+
return `${dbName}/${storeName}`;
|
|
7
|
+
}
|
|
8
|
+
export function getOrCreateIdbStore(dbName = DEFAULT_DB, storeName = DEFAULT_STORE) {
|
|
9
|
+
if (typeof indexedDB === "undefined")
|
|
10
|
+
return null;
|
|
11
|
+
const k = keyFor(dbName, storeName);
|
|
12
|
+
if (storeCache.has(k))
|
|
13
|
+
return storeCache.get(k);
|
|
14
|
+
try {
|
|
15
|
+
const s = createStore(dbName, storeName);
|
|
16
|
+
storeCache.set(k, s);
|
|
17
|
+
return s;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
storeCache.set(k, null);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function idbGet(key, opts) {
|
|
25
|
+
const s = getOrCreateIdbStore(opts?.dbName, opts?.storeName);
|
|
26
|
+
if (!s)
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
return await get(key, s);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function idbSet(key, value, opts) {
|
|
36
|
+
const s = getOrCreateIdbStore(opts?.dbName, opts?.storeName);
|
|
37
|
+
if (!s)
|
|
38
|
+
return;
|
|
39
|
+
try {
|
|
40
|
+
value === null ? await del(key, s) : await set(key, value, s);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* swallow to keep UI responsive */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type StoreState<T> = {
|
|
2
|
+
value: T | null | undefined;
|
|
3
|
+
repairTo: T | null;
|
|
4
|
+
hydrated: boolean;
|
|
5
|
+
listeners: Set<() => void>;
|
|
6
|
+
};
|
|
7
|
+
type Ns = {
|
|
8
|
+
dbName?: string;
|
|
9
|
+
storeName?: string;
|
|
10
|
+
};
|
|
11
|
+
type T_GetStore<T> = StoreState<T> & {
|
|
12
|
+
hydrate: () => void;
|
|
13
|
+
setValue: (next: T | null) => void;
|
|
14
|
+
isEqual: (a: unknown, b: unknown) => boolean;
|
|
15
|
+
};
|
|
16
|
+
export declare function getStore<T>(key: string, initial: T | null, isValid: (v: T | null) => boolean, isEqual: (a: T | null | undefined, b: T | null | undefined) => boolean, ns?: Ns): T_GetStore<T>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getBroadcastChannel } from "./broadcast";
|
|
2
|
+
import { DEFAULT_DB, DEFAULT_STORE, idbGet, idbSet } from "./idb";
|
|
3
|
+
// Composite registry key: namespace + logical key
|
|
4
|
+
function regKey(key, ns) {
|
|
5
|
+
const db = ns.dbName ?? DEFAULT_DB;
|
|
6
|
+
const st = ns.storeName ?? DEFAULT_STORE;
|
|
7
|
+
return `${db}/${st}|${key}`;
|
|
8
|
+
}
|
|
9
|
+
const stores = new Map();
|
|
10
|
+
export function getStore(key, initial, isValid, isEqual, ns) {
|
|
11
|
+
const k = regKey(key, ns ?? {});
|
|
12
|
+
if (stores.has(k))
|
|
13
|
+
return stores.get(k);
|
|
14
|
+
const state = {
|
|
15
|
+
value: undefined,
|
|
16
|
+
repairTo: initial,
|
|
17
|
+
hydrated: false,
|
|
18
|
+
listeners: new Set(),
|
|
19
|
+
};
|
|
20
|
+
const notify = () => {
|
|
21
|
+
for (const fn of state.listeners)
|
|
22
|
+
fn();
|
|
23
|
+
};
|
|
24
|
+
const persist = async (val) => {
|
|
25
|
+
await idbSet(key, val, ns);
|
|
26
|
+
};
|
|
27
|
+
const setValue = (next) => {
|
|
28
|
+
const safe = isValid(next) ? next : state.repairTo;
|
|
29
|
+
if (isEqual(state.value, safe))
|
|
30
|
+
return;
|
|
31
|
+
state.value = safe;
|
|
32
|
+
(async () => {
|
|
33
|
+
await persist(safe);
|
|
34
|
+
getBroadcastChannel(ns)?.postMessage({
|
|
35
|
+
key,
|
|
36
|
+
value: safe,
|
|
37
|
+
removed: safe === null,
|
|
38
|
+
});
|
|
39
|
+
})();
|
|
40
|
+
notify();
|
|
41
|
+
};
|
|
42
|
+
const hydrate = () => {
|
|
43
|
+
if (state.hydrated)
|
|
44
|
+
return;
|
|
45
|
+
state.hydrated = true;
|
|
46
|
+
// client-only work; on server this early return avoids starting async tasks
|
|
47
|
+
if (typeof window === "undefined")
|
|
48
|
+
return;
|
|
49
|
+
(async () => {
|
|
50
|
+
const persisted = (await idbGet(key, ns)) ?? state.repairTo;
|
|
51
|
+
const finalVal = isValid(persisted) ? persisted : state.repairTo;
|
|
52
|
+
state.value = finalVal;
|
|
53
|
+
if (!isEqual(persisted, finalVal)) {
|
|
54
|
+
try {
|
|
55
|
+
await persist(finalVal);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* noop */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
notify();
|
|
62
|
+
})();
|
|
63
|
+
const bc = getBroadcastChannel(ns);
|
|
64
|
+
if (bc) {
|
|
65
|
+
const handler = (event) => {
|
|
66
|
+
const d = event?.data;
|
|
67
|
+
if (!d || d.key !== key)
|
|
68
|
+
return;
|
|
69
|
+
const incoming = d.removed ? state.repairTo : d.value;
|
|
70
|
+
const safe = isValid(incoming) ? incoming : state.repairTo;
|
|
71
|
+
if (!isEqual(state.value, safe)) {
|
|
72
|
+
state.value = safe;
|
|
73
|
+
notify();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
bc.addEventListener("message", handler);
|
|
77
|
+
// no teardown needed for app lifetime; add resetStores() if you need HMR/test cleanup
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const store = Object.assign(state, {
|
|
81
|
+
hydrate,
|
|
82
|
+
setValue: (n) => setValue(n),
|
|
83
|
+
isEqual: (a, b) => isEqual(a, b),
|
|
84
|
+
});
|
|
85
|
+
stores.set(k, store);
|
|
86
|
+
return store;
|
|
87
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
export type T_UseLocalDb<T> = {
|
|
3
|
+
key: string;
|
|
4
|
+
schema: z.ZodSchema<T>;
|
|
5
|
+
initialValue?: T | null;
|
|
6
|
+
/** Optional: choose a specific IndexedDB database & object store */
|
|
7
|
+
dbName?: string;
|
|
8
|
+
storeName?: string;
|
|
9
|
+
};
|
|
10
|
+
export type UseLocalDbReturn<T> = {
|
|
11
|
+
value: T | null | undefined;
|
|
12
|
+
setValue: (next: T | null) => void;
|
|
13
|
+
};
|
|
14
|
+
export declare const validateWithSchema: <T>(schema: z.ZodSchema<T>, val: T | null) => boolean;
|
|
15
|
+
export declare function useLocalDb<T>({ key, schema, initialValue, dbName, storeName }: T_UseLocalDb<T>): UseLocalDbReturn<T>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { getStore } from "./components/store-registry";
|
|
3
|
+
import { isDeepEqual } from "@bb-labs/deep-equal";
|
|
4
|
+
export const validateWithSchema = (schema, val) => {
|
|
5
|
+
return val === null ? true : schema.safeParse(val).success;
|
|
6
|
+
};
|
|
7
|
+
export function useLocalDb({ key, schema, initialValue = null, dbName, storeName }) {
|
|
8
|
+
const initialRef = useRef(initialValue);
|
|
9
|
+
const ns = useMemo(() => ({ dbName, storeName }), [dbName, storeName]);
|
|
10
|
+
const isValid = useCallback((val) => validateWithSchema(schema, val), [schema]);
|
|
11
|
+
const isEqual = isDeepEqual;
|
|
12
|
+
const store = useMemo(() => getStore(key, initialRef.current, isValid, isEqual, ns), [key, isValid, isEqual, ns]);
|
|
13
|
+
const subscribe = useCallback((listener) => {
|
|
14
|
+
store.listeners.add(listener);
|
|
15
|
+
return () => store.listeners.delete(listener);
|
|
16
|
+
}, [store]);
|
|
17
|
+
const getSnapshot = useCallback(() => store.value, [store]);
|
|
18
|
+
const value = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
19
|
+
const setValue = useCallback((next) => {
|
|
20
|
+
if (store.isEqual(value, next))
|
|
21
|
+
return;
|
|
22
|
+
const safe = isValid(next) ? next : store.repairTo;
|
|
23
|
+
store.setValue(safe);
|
|
24
|
+
}, [store, value, isValid]);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
store.hydrate();
|
|
27
|
+
}, [store]);
|
|
28
|
+
return { value, setValue };
|
|
29
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./app/use-local-db";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./app/use-local-db";
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bb-labs/local-db",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A library for local database using IndexedDB and Zod for React",
|
|
5
|
+
"homepage": "https://github.com/beepbop-labs/local-db",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"local-db",
|
|
8
|
+
"indexeddb",
|
|
9
|
+
"keyval",
|
|
10
|
+
"zod",
|
|
11
|
+
"react",
|
|
12
|
+
"javascript",
|
|
13
|
+
"typescript",
|
|
14
|
+
"js",
|
|
15
|
+
"ts"
|
|
16
|
+
],
|
|
17
|
+
"author": "Beepbop",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/beepbop-labs/local-db.git"
|
|
22
|
+
},
|
|
23
|
+
"main": "dist/index.js",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"clean": "rm -rf dist",
|
|
30
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
31
|
+
"pack": "npm run build && npm pack --pack-destination ./archive/"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/bun": "latest",
|
|
35
|
+
"@types/react": "^19.2.7"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": "^19",
|
|
39
|
+
"typescript": "^5"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@bb-labs/deep-equal": "^0.0.1",
|
|
43
|
+
"fast-deep-equal": "^3.1.3",
|
|
44
|
+
"idb-keyval": "^6.2.2",
|
|
45
|
+
"zod": "^4.1.12"
|
|
46
|
+
}
|
|
47
|
+
}
|