@jeanharo98/typed-storage 0.1.5 → 0.1.6

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 CHANGED
@@ -160,7 +160,56 @@ createStorage(schema, {
160
160
 
161
161
  ---
162
162
 
163
- ## ⚙️ Options
163
+ ## 🗄️ Heavy data with IndexedDB
164
+
165
+ For large datasets that exceed `localStorage`'s ~5MB limit (file lists, extensive history, large collections), use `createHeavyStorage` — a separate async API backed by IndexedDB.
166
+
167
+ ```typescript
168
+ import { createHeavyStorage } from 'typed-storage';
169
+
170
+ const heavyStorage = createHeavyStorage({
171
+ documents: [] as Document[],
172
+ userPhotos: [] as Photo[]
173
+ }, {
174
+ dbName: 'myapp-storage',
175
+ ttl: 86400000 // optional — same TTL support as the sync API
176
+ });
177
+
178
+ // All operations are async — IndexedDB is asynchronous by nature
179
+ await heavyStorage.documents.set([...manyDocuments]);
180
+ const docs = await heavyStorage.documents.get();
181
+ await heavyStorage.documents.remove();
182
+
183
+ heavyStorage.documents.onChange((newValue) => {
184
+ console.log('documents changed:', newValue);
185
+ });
186
+
187
+ await heavyStorage.clear();
188
+ ```
189
+
190
+ ### Why a separate API?
191
+
192
+ `createStorage()` uses a synchronous Signal-like API by design — that's the core value of typed-storage. IndexedDB is asynchronous by nature, so mixing it into the same API would break that synchronous contract.
193
+
194
+ ```
195
+ createStorage() → sync, Signal-like, for UI preferences and small state
196
+ createHeavyStorage() → async, Promise-based, for large datasets
197
+ ```
198
+
199
+ If you only need small values (theme, language, settings), stick with `createStorage()`. Use `createHeavyStorage()` only when you specifically need to store data beyond localStorage's size limits.
200
+
201
+ ### `HeavySignal<T>` API
202
+
203
+ | Member | Description |
204
+ |--------|-------------|
205
+ | `signal.get()` | Returns a Promise with the current value |
206
+ | `signal.set(value)` | Stores the value, returns a Promise |
207
+ | `signal.remove()` | Deletes the value, returns a Promise |
208
+ | `signal.onChange(cb)` | Subscribes to value changes (called synchronously after set/remove) |
209
+
210
+ ---
211
+
212
+
164
213
 
165
214
  ```typescript
166
215
  const appStorage = createStorage(schema, options);
@@ -0,0 +1,19 @@
1
+ interface HeavyStorageOptions {
2
+ dbName?: string;
3
+ ttl?: number;
4
+ }
5
+ interface HeavySignal<T> {
6
+ get(): Promise<T>;
7
+ set(value: T): Promise<void>;
8
+ remove(): Promise<void>;
9
+ onChange(callback: (value: T) => void): void;
10
+ }
11
+ export declare function createHeavySignal<T>(key: string, initialValue: T, options?: HeavyStorageOptions): HeavySignal<T>;
12
+ type HeavyStorageSchema = Record<string, any>;
13
+ type HeavyStorageResult<T extends HeavyStorageSchema> = {
14
+ [K in keyof T]: HeavySignal<T[K]>;
15
+ } & {
16
+ clear(): Promise<void>;
17
+ };
18
+ export declare function createHeavyStorage<T extends HeavyStorageSchema>(schema: T, options?: HeavyStorageOptions): HeavyStorageResult<T>;
19
+ export {};
@@ -0,0 +1,89 @@
1
+ function openDB(dbName) {
2
+ return new Promise((resolve, reject) => {
3
+ const request = indexedDB.open(dbName, 1);
4
+ request.onupgradeneeded = (event) => {
5
+ const db = event.target.result;
6
+ if (!db.objectStoreNames.contains('storage')) {
7
+ db.createObjectStore('storage');
8
+ }
9
+ };
10
+ request.onsuccess = () => resolve(request.result);
11
+ request.onerror = () => reject(request.error);
12
+ });
13
+ }
14
+ function dbGet(db, key) {
15
+ return new Promise((resolve, reject) => {
16
+ const transaction = db.transaction('storage', 'readonly');
17
+ const store = transaction.objectStore('storage');
18
+ const request = store.get(key);
19
+ request.onsuccess = () => resolve(request.result);
20
+ request.onerror = () => reject(request.error);
21
+ });
22
+ }
23
+ function dbSet(db, key, value) {
24
+ return new Promise((resolve, reject) => {
25
+ const transaction = db.transaction('storage', 'readwrite');
26
+ const store = transaction.objectStore('storage');
27
+ const request = store.put(value, key);
28
+ request.onsuccess = () => resolve();
29
+ request.onerror = () => reject(request.error);
30
+ });
31
+ }
32
+ function dbDelete(db, key) {
33
+ return new Promise((resolve, reject) => {
34
+ const transaction = db.transaction('storage', 'readwrite');
35
+ const store = transaction.objectStore('storage');
36
+ const request = store.delete(key);
37
+ request.onsuccess = () => resolve();
38
+ request.onerror = () => reject(request.error);
39
+ });
40
+ }
41
+ export function createHeavySignal(key, initialValue, options) {
42
+ const dbName = options?.dbName ?? 'typed-storage-heavy';
43
+ const listeners = [];
44
+ function notify(value) {
45
+ listeners.forEach(cb => cb(value));
46
+ }
47
+ const signal = {};
48
+ signal.get = async function () {
49
+ const db = await openDB(dbName);
50
+ const stored = await dbGet(db, key);
51
+ if (stored === undefined)
52
+ return initialValue;
53
+ if (stored.expiresAt && Date.now() > stored.expiresAt) {
54
+ await dbDelete(db, key);
55
+ return initialValue;
56
+ }
57
+ return stored.value ?? initialValue;
58
+ };
59
+ signal.set = async function (value) {
60
+ const db = await openDB(dbName);
61
+ await dbSet(db, key, {
62
+ value,
63
+ expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
64
+ });
65
+ notify(value);
66
+ };
67
+ signal.remove = async function () {
68
+ const db = await openDB(dbName);
69
+ await dbDelete(db, key);
70
+ notify(initialValue);
71
+ };
72
+ signal.onChange = function (callback) {
73
+ listeners.push(callback);
74
+ };
75
+ return signal;
76
+ }
77
+ export function createHeavyStorage(schema, options) {
78
+ const result = {};
79
+ const keys = Object.keys(schema);
80
+ for (const key of keys) {
81
+ result[key] = createHeavySignal(key, schema[key], options);
82
+ }
83
+ result.clear = async () => {
84
+ for (const key of keys) {
85
+ await result[key].remove();
86
+ }
87
+ };
88
+ return result;
89
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { createStorage } from './create-storage.js';
2
+ export { createHeavyStorage } from './heavy-storage.js';
2
3
  export type { StorageSignal, StorageSchema, StorageResult, StorageSignalOptions } from './types.js';
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { createStorage } from './create-storage.js';
2
+ export { createHeavyStorage } from './heavy-storage.js';
package/index.html CHANGED
@@ -5,30 +5,41 @@
5
5
  </head>
6
6
  <body>
7
7
  <script type="module">
8
- import { createStorage } from './dist/index.js';
8
+ import { createHeavyStorage } from './dist/index.js';
9
9
 
10
- const compressedStorage = createStorage({
11
- bigData: { items: [] }
10
+ const heavyStorage = createHeavyStorage({
11
+ documents: [],
12
+ userPhotos: []
12
13
  }, {
13
- prefix: 'compressed',
14
- compress: true
14
+ dbName: 'test-heavy-storage'
15
15
  });
16
16
 
17
- // Genera datos grandes para probar
18
- const bigArray = Array.from({ length: 100 }, (_, i) => ({
19
- id: i,
20
- name: `Item ${i}`,
21
- description: 'Lorem ipsum dolor sit amet consectetur'
22
- }));
17
+ async function test() {
18
+ // Set
19
+ await heavyStorage.documents.set([
20
+ { id: 1, name: 'Documento 1' },
21
+ { id: 2, name: 'Documento 2' }
22
+ ]);
23
+ console.log('✅ Documentos guardados');
23
24
 
24
- compressedStorage.bigData.set({ items: bigArray });
25
+ // Get
26
+ const docs = await heavyStorage.documents.get();
27
+ console.log('📄 Documentos leídos:', docs);
25
28
 
26
- console.log('Valor leído:', compressedStorage.bigData());
27
- console.log('Tamaño en localStorage:', localStorage.getItem('compressed:bigData')?.length, 'caracteres');
29
+ // onChange
30
+ heavyStorage.documents.onChange((newValue) => {
31
+ console.log('🔔 documents cambió a:', newValue);
32
+ });
28
33
 
29
- // Compara con la versión sin comprimir
30
- const uncompressedSize = JSON.stringify({ value: { items: bigArray } }).length;
31
- console.log('Tamaño sin comprimir sería:', uncompressedSize, 'caracteres');
34
+ await heavyStorage.documents.set([{ id: 3, name: 'Documento nuevo' }]);
35
+
36
+ // Remove
37
+ await heavyStorage.userPhotos.remove();
38
+ const photos = await heavyStorage.userPhotos.get();
39
+ console.log('🗑️ userPhotos después de remove:', photos);
40
+ }
41
+
42
+ test();
32
43
  </script>
33
44
  </body>
34
45
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeanharo98/typed-storage",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Type-safe localStorage with reactive signals",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "devDependencies": {
18
18
  "@vitest/coverage-v8": "^4.1.8",
19
+ "fake-indexeddb": "^6.2.5",
19
20
  "jsdom": "^29.1.1",
20
21
  "ts-node": "^10.9.2",
21
22
  "typescript": "^6.0.3",
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { createHeavyStorage } from './heavy-storage.js';
3
+
4
+ describe('createHeavyStorage', () => {
5
+
6
+ it('debe retornar el initialValue si no hay datos guardados', async () => {
7
+ // crea heavyStorage con dbName único para este test
8
+ const heavyStorage = createHeavyStorage({
9
+ documents: [],
10
+ userPhotos: []
11
+ }, {
12
+ dbName: 'test-heavy-storage'
13
+ });
14
+
15
+ // verifica que get() retorna el initialValue
16
+ const result = await heavyStorage.documents.get();
17
+ expect(result).toEqual([]);
18
+ });
19
+
20
+ it('debe guardar y leer un valor con set/get', async () => {
21
+ // crea heavyStorage
22
+ const heavyStorage = createHeavyStorage({
23
+ documents: [] as any[]
24
+ }, {
25
+ dbName: 'test-db-2'
26
+ });
27
+
28
+
29
+ // llama set() con un valor
30
+ const newDocs = [{ id: 1, name: 'Doc 1' }];
31
+ await heavyStorage.documents.set(newDocs);
32
+
33
+ // verifica que get() retorna ese valor
34
+ const result = await heavyStorage.documents.get();
35
+ expect(result).toEqual(newDocs);
36
+ });
37
+
38
+ it('debe eliminar un valor con remove()', async () => {
39
+ // crea heavyStorage, set() un valor
40
+ const heavyStorage = createHeavyStorage({
41
+ documents: [] as any[]
42
+ }, {
43
+ dbName: 'test-db-3'
44
+ });
45
+ await heavyStorage.documents.set([{ id: 1, name: 'Doc 1' }]);
46
+
47
+ // llama remove()
48
+ await heavyStorage.documents.remove();
49
+
50
+ // verifica que get() retorna el initialValue
51
+ const result = await heavyStorage.documents.get();
52
+ expect(result).toEqual([]);
53
+ });
54
+
55
+ it('debe notificar a onChange cuando se llama set()', async () => {
56
+ // usa vi.fn() como callback
57
+ const heavyStorage = createHeavyStorage({
58
+ documents: [] as any[]
59
+ }, {
60
+ dbName: 'test-db-4'
61
+ });
62
+ const callback = vi.fn();
63
+
64
+ // registra onChange
65
+ heavyStorage.documents.onChange(callback);
66
+
67
+ // llama set()
68
+ const newDocs = [{ id: 1, name: 'Doc 1' }];
69
+ await heavyStorage.documents.set(newDocs);
70
+
71
+ // verifica que el callback fue llamado con el valor correcto
72
+ expect(callback).toHaveBeenCalledWith(newDocs);
73
+ });
74
+
75
+ it('clear() debe resetear todas las keys del schema', async () => {
76
+ // crea heavyStorage con 2 keys
77
+ const heavyStorage = createHeavyStorage({
78
+ documents: [] as any[],
79
+ userPhotos: [] as any[]
80
+ }, {
81
+ dbName: 'test-db-5'
82
+ });
83
+
84
+ // set() en ambas
85
+ await heavyStorage.documents.set([{ id: 1 }]);
86
+ await heavyStorage.userPhotos.set([{ url: 'photo.jpg' }]);
87
+
88
+ // llama clear()
89
+ await heavyStorage.clear();
90
+
91
+ // verifica que ambas vuelven a su initialValue
92
+ const docs = await heavyStorage.documents.get();
93
+ const photos = await heavyStorage.userPhotos.get();
94
+
95
+ expect(docs).toEqual([]);
96
+ expect(photos).toEqual([]);
97
+ });
98
+
99
+ });
@@ -0,0 +1,146 @@
1
+ // Abrir/crear la base de datos
2
+ function openDB(dbName: string): Promise<IDBDatabase> {
3
+ return new Promise((resolve, reject) => {
4
+ const request = indexedDB.open(dbName, 1);
5
+
6
+ request.onupgradeneeded = (event) => {
7
+ const db = (event.target as IDBOpenDBRequest).result;
8
+ // Crea el "object store" — equivalente a una tabla
9
+ if (!db.objectStoreNames.contains('storage')) {
10
+ db.createObjectStore('storage');
11
+ }
12
+ };
13
+
14
+ request.onsuccess = () => resolve(request.result);
15
+ request.onerror = () => reject(request.error);
16
+ });
17
+ }
18
+
19
+ function dbGet(db: IDBDatabase, key: string): Promise<any> {
20
+ return new Promise((resolve, reject) => {
21
+ const transaction = db.transaction('storage', 'readonly');
22
+ const store = transaction.objectStore('storage');
23
+ const request = store.get(key);
24
+
25
+ request.onsuccess = () => resolve(request.result);
26
+ request.onerror = () => reject(request.error);
27
+ });
28
+ }
29
+
30
+ function dbSet(db: IDBDatabase, key: string, value: any): Promise<void> {
31
+ return new Promise((resolve, reject) => {
32
+ const transaction = db.transaction('storage', 'readwrite');
33
+ const store = transaction.objectStore('storage');
34
+ const request = store.put(value, key);
35
+
36
+ request.onsuccess = () => resolve();
37
+ request.onerror = () => reject(request.error);
38
+ });
39
+ }
40
+
41
+ function dbDelete(db: IDBDatabase, key: string): Promise<void> {
42
+ return new Promise((resolve, reject) => {
43
+ const transaction = db.transaction('storage', 'readwrite');
44
+ const store = transaction.objectStore('storage');
45
+ const request = store.delete(key);
46
+
47
+ request.onsuccess = () => resolve();
48
+ request.onerror = () => reject(request.error);
49
+ });
50
+ }
51
+
52
+ // ===========================================
53
+
54
+ interface HeavyStorageOptions {
55
+ dbName?: string;
56
+ ttl?: number;
57
+ }
58
+
59
+ interface HeavySignal<T> {
60
+ get(): Promise<T>;
61
+ set(value: T): Promise<void>;
62
+ remove(): Promise<void>;
63
+ onChange(callback: (value: T) => void): void;
64
+ }
65
+
66
+ export function createHeavySignal<T>(
67
+ key: string,
68
+ initialValue: T,
69
+ options?: HeavyStorageOptions
70
+ ): HeavySignal<T> {
71
+ const dbName = options?.dbName ?? 'typed-storage-heavy';
72
+ const listeners: Array<(value: T) => void> = [];
73
+
74
+ function notify(value: T): void {
75
+ listeners.forEach(cb => cb(value));
76
+ }
77
+
78
+ const signal: any = {};
79
+
80
+ signal.get = async function(): Promise<T> {
81
+ const db = await openDB(dbName);
82
+ const stored = await dbGet(db, key);
83
+
84
+ if (stored === undefined) return initialValue;
85
+
86
+ // Verifica TTL si existe
87
+ if (stored.expiresAt && Date.now() > stored.expiresAt) {
88
+ await dbDelete(db, key);
89
+ return initialValue;
90
+ }
91
+
92
+ return stored.value ?? initialValue;
93
+ };
94
+
95
+ signal.set = async function(value: T): Promise<void> {
96
+ const db = await openDB(dbName);
97
+ await dbSet(db, key, {
98
+ value,
99
+ expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
100
+ });
101
+ notify(value);
102
+ };
103
+
104
+ signal.remove = async function(): Promise<void> {
105
+ const db = await openDB(dbName);
106
+ await dbDelete(db, key);
107
+ notify(initialValue);
108
+ };
109
+
110
+ signal.onChange = function(callback: (value: T) => void): void {
111
+ listeners.push(callback);
112
+ };
113
+
114
+ return signal as HeavySignal<T>;
115
+ }
116
+
117
+ // ===========================================
118
+ // Iteramos el schema completo
119
+
120
+ type HeavyStorageSchema = Record<string, any>;
121
+
122
+ type HeavyStorageResult<T extends HeavyStorageSchema> = {
123
+ [K in keyof T]: HeavySignal<T[K]>;
124
+ } & {
125
+ clear(): Promise<void>;
126
+ };
127
+
128
+ export function createHeavyStorage<T extends HeavyStorageSchema>(
129
+ schema: T,
130
+ options?: HeavyStorageOptions
131
+ ): HeavyStorageResult<T> {
132
+ const result: any = {};
133
+
134
+ const keys = Object.keys(schema);
135
+ for (const key of keys) {
136
+ result[key] = createHeavySignal(key, schema[key], options);
137
+ }
138
+
139
+ result.clear = async () => {
140
+ for (const key of keys) {
141
+ await result[key].remove();
142
+ }
143
+ };
144
+
145
+ return result as HeavyStorageResult<T>;
146
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,8 @@
1
1
  export { createStorage } from './create-storage.js';
2
- export type { StorageSignal, StorageSchema, StorageResult, StorageSignalOptions } from './types.js';
2
+ export { createHeavyStorage } from './heavy-storage.js';
3
+ export type {
4
+ StorageSignal,
5
+ StorageSchema,
6
+ StorageResult,
7
+ StorageSignalOptions
8
+ } from './types.js';
package/vitest.config.ts CHANGED
@@ -4,5 +4,6 @@ export default defineConfig({
4
4
  test: {
5
5
  environment: 'jsdom',
6
6
  globals: true,
7
+ setupFiles: ['./vitest.setup.ts']
7
8
  }
8
9
  });
@@ -0,0 +1 @@
1
+ import 'fake-indexeddb/auto';