@jeanharo98/typed-storage 0.1.3 → 0.1.5

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
@@ -174,10 +174,45 @@ const appStorage = createStorage(schema, options);
174
174
  | `sync` | `boolean` | `false` | Sync values across browser tabs via `StorageEvent` |
175
175
  | `version` | `number` | — | Current schema version — required for migrations |
176
176
  | `migrations` | `Record<number, (data) => data>` | — | Migration functions per version |
177
+ | `compress` | `boolean` | `false` | Compresses data with LZ-string before storing |
177
178
  | `encrypt` | `boolean` | `false` | Shows a security warning — see note below |
178
179
 
179
180
  ---
180
181
 
182
+ ## 📦 Compression
183
+
184
+ For large or repetitive data (lists, history, complex objects), enable compression to reduce the space used in `localStorage`:
185
+
186
+ ```typescript
187
+ const appStorage = createStorage({
188
+ cart: { items: [] }
189
+ }, {
190
+ prefix: 'shop',
191
+ compress: true
192
+ });
193
+
194
+ appStorage.cart.set({ items: [...manyProducts] });
195
+ // Data is compressed with LZ-string before saving
196
+ // and decompressed automatically when read
197
+ ```
198
+
199
+ ### When to use it
200
+
201
+ ```
202
+ ✅ Useful for:
203
+ - Large lists (shopping carts, history)
204
+ - Repetitive JSON structures
205
+ - Data approaching localStorage's ~5MB limit
206
+
207
+ ❌ Not needed for:
208
+ - Small values like theme, language, fontSize
209
+ - The compression overhead isn't worth it for tiny data
210
+ ```
211
+
212
+ Compression only runs when `compress: true` is explicitly set — there's zero overhead for the default use case.
213
+
214
+ ---
215
+
181
216
  ## 🔔 onChange
182
217
 
183
218
  Subscribe to changes on any key:
@@ -297,6 +332,7 @@ Creates a storage object from a schema. Returns a `StorageResult<T>` with one `S
297
332
  |---------|-------------|
298
333
  | [@jeanharo98/typed-storage-angular](https://github.com/JeanHaro/typed-storage-angular) | Angular wrapper with native Signals |
299
334
  | [@jeanharo98/typed-storage-react](https://github.com/JeanHaro/typed-storage-react) | React wrapper with useStorage() hook |
335
+ | [typed-storage-devtools](https://github.com/JeanHaro/typed-storage-devtools) | Chrome DevTools extension for real-time inspection |
300
336
 
301
337
  ---
302
338
 
@@ -1,11 +1,22 @@
1
1
  import { createStorageSignal } from './storage-signal.js';
2
2
  import { applyMigrations } from './migrations.js';
3
+ function registerPrefix(prefix, sto) {
4
+ const registryKey = '__typed-storage__';
5
+ const existing = sto.getItem(registryKey);
6
+ const prefixes = existing ? JSON.parse(existing) : [];
7
+ if (prefix && !prefixes.includes(prefix)) {
8
+ prefixes.push(prefix);
9
+ sto.setItem(registryKey, JSON.stringify(prefixes));
10
+ }
11
+ }
3
12
  export function createStorage(schema, options) {
4
13
  if (options?.version && options.migrations) {
5
14
  const sto = options.storage === 'session' ? sessionStorage : localStorage;
6
15
  const prefix = options.prefix ?? '';
7
16
  applyMigrations(prefix, options.version, options.migrations, sto);
8
17
  }
18
+ const sto = options?.storage === 'session' ? sessionStorage : localStorage;
19
+ registerPrefix(options?.prefix ?? '', sto);
9
20
  if (options?.encrypt) {
10
21
  console.warn(`
11
22
  ⚠️ typed-storage: la opción encrypt está activada.
@@ -1,3 +1,4 @@
1
+ import LZString from 'lz-string';
1
2
  import { MemoryStorage } from "./memory-storage.js";
2
3
  function safeParseJSON(value, fallback) {
3
4
  if (!value)
@@ -31,7 +32,10 @@ export function createStorageSignal(key, initialValue, options) {
31
32
  if (options?.prefix) {
32
33
  key = `${options.prefix}:${key}`;
33
34
  }
34
- const savedData = sto.getItem(key);
35
+ const rawData = sto.getItem(key);
36
+ const savedData = options?.compress && rawData
37
+ ? LZString.decompress(rawData)
38
+ : rawData;
35
39
  let currentValue;
36
40
  const listeners = [];
37
41
  function notify(value) {
@@ -60,7 +64,10 @@ export function createStorageSignal(key, initialValue, options) {
60
64
  notify(initialValue);
61
65
  return currentValue = initialValue;
62
66
  }
63
- const item = safeParseJSON(event.newValue, initialValue);
67
+ const rawNewValue = options?.compress
68
+ ? LZString.decompress(event.newValue)
69
+ : event.newValue;
70
+ const item = safeParseJSON(rawNewValue, initialValue);
64
71
  notify(item.value);
65
72
  return currentValue = item.value;
66
73
  }
@@ -69,15 +76,23 @@ export function createStorageSignal(key, initialValue, options) {
69
76
  signalBase.set = function (newValue) {
70
77
  currentValue = newValue;
71
78
  notify(currentValue);
72
- sto.setItem(key, JSON.stringify({
79
+ const dataToStore = JSON.stringify({
73
80
  value: newValue,
74
81
  expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
75
- }));
82
+ });
83
+ const finalData = options?.compress
84
+ ? LZString.compress(dataToStore)
85
+ : dataToStore;
86
+ sto.setItem(key, finalData);
76
87
  };
77
88
  signalBase.reset = function () {
78
89
  currentValue = initialValue;
79
90
  notify(currentValue);
80
- sto.setItem(key, JSON.stringify(initialValue));
91
+ const dataToStore = JSON.stringify(initialValue);
92
+ const finalData = options?.compress
93
+ ? LZString.compress(dataToStore)
94
+ : dataToStore;
95
+ sto.setItem(key, finalData);
81
96
  };
82
97
  signalBase.has = function () {
83
98
  return !!sto.getItem(key);
package/dist/types.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface StorageSignalOptions {
6
6
  encrypt?: boolean;
7
7
  version?: number;
8
8
  migrations?: Record<number, (data: any) => any>;
9
+ compress?: boolean;
9
10
  }
10
11
  export interface StorageSignal<T> {
11
12
  (): T;
package/index.html CHANGED
@@ -7,35 +7,28 @@
7
7
  <script type="module">
8
8
  import { createStorage } from './dist/index.js';
9
9
 
10
- // Simula datos viejos en localStorage (schema v1)
11
- localStorage.setItem('app:theme', '"dark"');
12
- localStorage.setItem('app:fontSize', '16');
13
- localStorage.setItem('app__version__', '1');
14
-
15
- // Crea el storage con schema v2 y migración
16
- const appStorage = createStorage({
17
- theme: 'dark',
18
- preferences: {
19
- fontSize: 16,
20
- language: 'es'
21
- }
10
+ const compressedStorage = createStorage({
11
+ bigData: { items: [] }
22
12
  }, {
23
- prefix: 'app',
24
- version: 2,
25
- migrations: {
26
- 1: (oldData) => ({
27
- theme: oldData.theme,
28
- preferences: {
29
- fontSize: oldData.fontSize,
30
- language: 'es'
31
- }
32
- })
33
- }
13
+ prefix: 'compressed',
14
+ compress: true
34
15
  });
35
16
 
36
- console.log('theme:', appStorage.theme());
37
- console.log('preferences:', appStorage.preferences());
38
- console.log('version en localStorage:', localStorage.getItem('app__version__'));
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
+ }));
23
+
24
+ compressedStorage.bigData.set({ items: bigArray });
25
+
26
+ console.log('Valor leído:', compressedStorage.bigData());
27
+ console.log('Tamaño en localStorage:', localStorage.getItem('compressed:bigData')?.length, 'caracteres');
28
+
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');
39
32
  </script>
40
33
  </body>
41
34
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeanharo98/typed-storage",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Type-safe localStorage with reactive signals",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,6 +19,7 @@
19
19
  "jsdom": "^29.1.1",
20
20
  "ts-node": "^10.9.2",
21
21
  "typescript": "^6.0.3",
22
+ "vite": "^8.0.16",
22
23
  "vitest": "^4.1.8"
23
24
  },
24
25
  "repository": {
@@ -29,10 +30,14 @@
29
30
  "bugs": {
30
31
  "url": "https://github.com/JeanHaro/typed-storage/issues"
31
32
  },
33
+ "dependencies": {
34
+ "lz-string": "^1.5.0"
35
+ },
32
36
  "scripts": {
33
37
  "dev": "ts-node src/index.ts",
34
38
  "build": "tsc",
35
39
  "test": "vitest",
36
- "test:coverage": "vitest --coverage"
40
+ "test:coverage": "vitest --coverage",
41
+ "serve": "vite"
37
42
  }
38
43
  }
@@ -7,10 +7,25 @@ import { createStorageSignal } from './storage-signal.js';
7
7
  // Migraciones
8
8
  import { applyMigrations } from './migrations.js';
9
9
 
10
+ function registerPrefix (
11
+ prefix: string,
12
+ sto: Storage
13
+ ): void {
14
+ const registryKey = '__typed-storage__';
15
+ const existing = sto.getItem(registryKey);
16
+ const prefixes: string[] = existing ? JSON.parse(existing) : [];
17
+
18
+ if ( prefix && !prefixes.includes(prefix) ) {
19
+ prefixes.push(prefix);
20
+ sto.setItem(registryKey, JSON.stringify(prefixes));
21
+ }
22
+ }
23
+
10
24
  export function createStorage<T extends StorageSchema>(
11
25
  schema: T,
12
26
  options?: StorageSignalOptions
13
27
  ): StorageResult<T> {
28
+ // Migraciones
14
29
  if ( options?.version && options.migrations ) {
15
30
  const sto = options.storage === 'session' ? sessionStorage : localStorage;
16
31
  const prefix = options.prefix ?? '';
@@ -23,6 +38,10 @@ export function createStorage<T extends StorageSchema>(
23
38
  );
24
39
  }
25
40
 
41
+ // Registramos el prefix en localStorage
42
+ const sto = options?.storage === 'session' ? sessionStorage : localStorage;
43
+ registerPrefix(options?.prefix ?? '', sto);
44
+
26
45
  if ( options?.encrypt ) {
27
46
  console.warn(`
28
47
  ⚠️ typed-storage: la opción encrypt está activada.
@@ -84,4 +84,38 @@ describe('onChange', () => {
84
84
  theme.reset();
85
85
  expect(callback).toHaveBeenCalledWith('dark');
86
86
  });
87
+ });
88
+
89
+ describe('compress option', () => {
90
+ beforeEach(() => localStorage.clear());
91
+
92
+ it('debe comprimir y descomprimir correctamente', () => {
93
+ const signal = createStorageSignal<{ items: any[] }>(
94
+ 'data',
95
+ { items: [] },
96
+ { compress: true }
97
+ );
98
+ const bigData = {
99
+ items: Array.from({ length: 50 },
100
+ (_, i) => ({ id: i }))
101
+ };
102
+
103
+ signal.set(bigData);
104
+
105
+ expect(signal()).toEqual(bigData);
106
+ });
107
+
108
+ it('el dato comprimido debe ocupar menos espacio que sin comprimir', () => {
109
+ const compressed = createStorageSignal('data1', '', { compress: true, prefix: 'c1' });
110
+ const uncompressed = createStorageSignal('data2', '', { prefix: 'c2' });
111
+
112
+ const repetitive = 'a'.repeat(1000);
113
+ compressed.set(repetitive);
114
+ uncompressed.set(repetitive);
115
+
116
+ const compressedSize = localStorage.getItem('c1:data1')!.length;
117
+ const uncompressedSize = localStorage.getItem('c2:data2')!.length;
118
+
119
+ expect(compressedSize).toBeLessThan(uncompressedSize);
120
+ });
87
121
  });
@@ -1,3 +1,5 @@
1
+ import LZString from 'lz-string';
2
+
1
3
  // Tipos
2
4
  import { StorageSignal, StorageSignalOptions } from "./types.js";
3
5
 
@@ -33,7 +35,7 @@ function safeParseJSON<T>(value: string, fallback: T): StoredValue<T> {
33
35
  }
34
36
  }
35
37
 
36
- // Obtenemos el valor del localStorage o SessionStorage, si sale error entonces se usará el MemoryStorage
38
+ // Obtenemos el valor del localStorage o SessionStorage, sino MemoryStorage
37
39
  function getStorage(type: 'local' | 'session'): Storage | MemoryStorage {
38
40
  try {
39
41
  const sto = type === 'session' ? sessionStorage : localStorage;
@@ -63,7 +65,10 @@ export function createStorageSignal<T>(
63
65
  key = `${options.prefix}:${key}`;
64
66
  }
65
67
 
66
- const savedData = sto.getItem(key);
68
+ const rawData = sto.getItem(key);
69
+ const savedData = options?.compress && rawData
70
+ ? LZString.decompress(rawData)
71
+ : rawData;
67
72
  let currentValue: T;
68
73
 
69
74
  const listeners: Array<(value: T) => void> = [];
@@ -101,7 +106,10 @@ export function createStorageSignal<T>(
101
106
  }
102
107
 
103
108
  // Parseamos el nuevo valor
104
- const item = safeParseJSON(event.newValue, initialValue);
109
+ const rawNewValue = options?.compress
110
+ ? LZString.decompress(event.newValue)
111
+ : event.newValue;
112
+ const item = safeParseJSON(rawNewValue, initialValue);
105
113
 
106
114
  notify(item.value as T);
107
115
  return currentValue = item.value as T;
@@ -112,19 +120,29 @@ export function createStorageSignal<T>(
112
120
  signalBase.set = function ( newValue: T ): void {
113
121
  currentValue = newValue;
114
122
  notify(currentValue);
115
- sto.setItem(
116
- key,
117
- JSON.stringify({
118
- value: newValue,
119
- expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
120
- })
121
- );
123
+
124
+ const dataToStore = JSON.stringify({
125
+ value: newValue,
126
+ expiresAt: options?.ttl ? Date.now() + options.ttl : undefined
127
+ });
128
+
129
+ const finalData = options?.compress
130
+ ? LZString.compress(dataToStore)
131
+ : dataToStore;
132
+
133
+ sto.setItem( key, finalData );
122
134
  }
123
135
 
124
136
  signalBase.reset = function(): void {
125
137
  currentValue = initialValue;
126
138
  notify(currentValue);
127
- sto.setItem(key, JSON.stringify(initialValue));
139
+
140
+ const dataToStore = JSON.stringify(initialValue);
141
+ const finalData = options?.compress
142
+ ? LZString.compress(dataToStore)
143
+ : dataToStore;
144
+
145
+ sto.setItem(key, finalData);
128
146
  }
129
147
 
130
148
  signalBase.has = function(): boolean {
package/src/types.ts CHANGED
@@ -7,6 +7,7 @@ export interface StorageSignalOptions {
7
7
  encrypt?: boolean;
8
8
  version?: number;
9
9
  migrations?: Record<number, ( data: any ) => any>;
10
+ compress?: boolean;
10
11
  }
11
12
 
12
13
  // Objeto reactivo con getter, setter y reset