hyperstorage-js 5.0.6 → 5.1.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A lightweight wrapper for Storage interfaces (e.g., `localStorage` or `sessionStorage`) with **efficient caching** and **type-preserving serialization**.
4
4
 
5
- The biggest burdens of working with the **Storage API** is verifying values on every read, providing proper default values and only being able to store strings, having to `JSON.stringify()` and `JSON.parse()` manually everytime. This package eliminates all of this by providing a safe and automatic wrapper that handles everything at once. You can read/store numbers and objects without any extra steps and lose no performance.
5
+ The biggest burdens of working with the **Storage API** is verifying values on every read, providing proper default values and only being able to store strings, having to `JSON.stringify()` and `JSON.parse()` manually everytime. This package eliminates this all by providing a safe, automatic and efficient wrapper that handles everything for you. You can read/store numbers and objects without any extra steps, lose no performance and improve code readability.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/hyperstorage-js.svg)](https://www.npmjs.com/package/hyperstorage-js)
8
8
  [![npm downloads](https://img.shields.io/npm/dt/hyperstorage-js.svg)](https://www.npmjs.com/package/hyperstorage-js)
@@ -13,7 +13,8 @@ The biggest burdens of working with the **Storage API** is verifying values on e
13
13
  ## Features
14
14
 
15
15
  - 📝 **Default values**: are automatically set when the key is not in Storage.
16
- - 🧩 **JSON support**: automatically serializes and parses objects or non-string primitives (`undefined`, `NaN`, `Infinity`) which the Storage API does not support by default.
16
+ - 🧩 **JSON support**: automatically serializes and parses objects or non-string primitives (numbers, `undefined`, `NaN`, etc.) which the Storage API does not support by default.
17
+ - 🛠️ **Utility helpers**: built-in helper methods (like `.set()` and `.isDefault()`) to simplify storage operations.
17
18
  - ⚡ **Fast caching**: memory cache avoids repeated JSON convertions.
18
19
  - 🔒 **Optional encoding/decoding** hooks to obfuscate data.
19
20
  - 🌐 **Custom storage**: works with any object implementing the standard Storage API. (`localStorage`, `sessionStorage`, ...)
@@ -38,7 +39,7 @@ yarn add hyperstorage-js
38
39
  ## Constructor Syntax
39
40
 
40
41
  ```ts
41
- class StorageManager<T> {
42
+ class HyperStorage<T> {
42
43
  constructor(
43
44
  itemName: string,
44
45
  defaultValue: T,
@@ -60,39 +61,38 @@ import HyperStorage from 'hyperstorage-js'
60
61
  ```
61
62
 
62
63
  ```js
63
- const defaultValue = { theme: 'dark', language: 'en' }
64
+ const defaultValue = { theme: 'light', language: 'en' }
64
65
  const userStore = new HyperStorage('userSettings', defaultValue)
65
66
 
66
67
  // If 'userSettings' is not present in the Storage, the defaultValue is set:
67
- console.log(userStore.value) // { theme: 'dark', language: 'en' }
68
+ console.log(userStore.value) // { theme: 'light', language: 'en' }
68
69
 
69
- // Change theme to light:
70
- userStore.value = { theme: 'light', language: 'en' }
70
+ // Change theme to dark
71
+ userStore.value = { theme: 'dark', language: 'en' }
72
+ // or
73
+ userStore.set((v) => (v.theme = 'dark'))
71
74
 
72
- console.log(userStore.value) // { theme: 'light' }
73
- console.log(userStore.value.theme) // 'light'
75
+ console.log(userStore.value) // { theme: 'dark', language: 'en' }
76
+ console.log(userStore.value.theme) // 'dark'
74
77
 
75
- // Present in localStorage:
76
- console.log(userStore.storage) // Storage {userSettings: '\x00{"theme":"light"}', length: 1}
78
+ // Present in localStorage
79
+ console.log(userStore.storage) // Storage {userSettings: '\x00{"theme":"dark","language":"en"}', length: 1}
77
80
  ```
78
81
 
79
82
  ### Different Ways to Assign a New Value
80
83
 
81
84
  ```js
82
- // Overwrite all
83
- userStore.value = { theme: 'light', language: 'en' }
84
-
85
- // Overwrite specific
86
- userStore.value = { ...userStore.value, theme: 'light' }
85
+ // Using setter
86
+ userStore.value = { theme: 'dark', language: 'en' }
87
87
 
88
- // Overwrite all using callback
89
- userStore.set((v) => (v = { theme: 'light', language: 'en' }))
88
+ // Change single property using the setter
89
+ userStore.value = { ...userStore.value, theme: 'dark' }
90
90
 
91
- // Overwrite specific using callback
92
- userStore.set((v) => (v.theme = 'light'))
91
+ // Change single property using a callback
92
+ userStore.set((v) => (v.theme = 'dark'))
93
93
 
94
- // Overwrite and store result
95
- const result = userStore.set((v) => (v.theme = 'light'))
94
+ // Change single property using a property setter
95
+ userStore.set('theme', 'dark')
96
96
  ```
97
97
 
98
98
  ### Using Another Storage API
@@ -152,15 +152,15 @@ console.log(sessionStore.storage) // Storage {length: 0}
152
152
 
153
153
  ```ts
154
154
  interface Settings {
155
- theme: 'dark' | 'light'
155
+ theme: 'system' | 'light' | 'dark'
156
156
  language: string
157
157
  }
158
158
 
159
- const defaultValue: Settings = { theme: 'dark', language: 'en' }
159
+ const defaultValue: Settings = { theme: 'system', language: 'en' }
160
160
  const userStore = new HyperStorage<Settings>('userSettings', defaultValue)
161
161
 
162
- // Property 'language' is missing in type '{ theme: "light"; }' but required in type 'Settings'. ts(2741)
163
- userStore.value = { theme: 'light' }
162
+ // Property 'language' is missing in type '{ theme: "dark"; }' but required in type 'Settings'. ts(2741)
163
+ userStore.value = { theme: 'dark' }
164
164
  ```
165
165
 
166
166
  ### Using `sync()`
@@ -168,14 +168,19 @@ userStore.value = { theme: 'light' }
168
168
  Safe usage of `sync()` requires explicit runtime validation before accessing any properties. It quickly becomes clear how type-unsafe `sync()` is and why it should be avoided.
169
169
 
170
170
  ```ts
171
- const current = userStore.sync() // (method): unknown
171
+ const result = userStore.sync() // (method): unknown
172
+ // Right now, 'result' equals 'userStore.value' exactly
172
173
 
173
- // 'current' is of type 'unknown'. ts(18046)
174
- console.log(current.theme) // { theme: 'light' }
174
+ // 'result' is of type 'unknown'. ts(18046)
175
+ console.log(result.theme) // 'dark'
175
176
 
176
- // Must narrow down
177
- if (current && typeof current === 'object' && 'theme' in current) {
178
- console.log(current.theme)
177
+ // No error, but unsafe
178
+ console.log(userStore.value.theme) // 'dark'
179
+
180
+ // Must narrow down to be safe
181
+ if (result && typeof result === 'object' && 'theme' in result) {
182
+ // No error, safe
183
+ console.log(result.theme) // 'dark'
179
184
  }
180
185
  ```
181
186
 
@@ -245,13 +250,66 @@ If the underlying `Storage` is not modified through the value setter, the intern
245
250
 
246
251
  ```js
247
252
  // External change to storage (to be avoided)
248
- localStorage.setItem('userSettings', '{"theme":"blue"}')
253
+ localStorage.setItem('userSettings', '{"theme":"dark"}')
249
254
 
250
255
  // Resynchronize the cache, optionally with a custom decoder
251
256
  userStore.sync((value) => JSON.parse(value))
252
257
 
253
- console.log(userStore.value) // { theme: 'blue' }
254
- console.log(userStore.storage) // Storage {userSettings: '\x00{"theme":"blue"}', length: 1}
258
+ console.log(userStore.value) // { theme: 'dark' }
259
+ console.log(userStore.storage) // Storage {userSettings: '\x00{"theme":"dark"}', length: 1}
260
+ ```
261
+
262
+ #### Why `sync()` is unsafe
263
+
264
+ 1. The item in `Storage` is undecodable by the `decodeFn` passed to `sync()`.
265
+
266
+ ```js
267
+ localStorage.setItem('userSettings', 'not an object')
268
+
269
+ // SyntaxError: Unexpected token 'o', "not an object" is not valid JSON
270
+ const result = userStore.sync((value) => JSON.parse(value))
271
+ // Execution continues because sync() uses a try-catch
272
+
273
+ console.log(result) // instance of SyntaxError
274
+
275
+ // Reset to default value
276
+ console.log(userStore.value) // {theme: 'system', language: 'en'}
277
+ ```
278
+
279
+ 2. The item in `Storage`, after being decoded, is not of type `T`.
280
+
281
+ ```js
282
+ localStorage.setItem('userSettings', '{"not":"valid"}')
283
+
284
+ const result = userStore.sync((value) => JSON.parse(value))
285
+ console.log(result) // {not: 'valid'}
286
+ console.log(userStore.value) // {not: 'valid'}
287
+ ```
288
+
289
+ No errors, but `result` and `userStore.value` are both not of type `T`.
290
+
291
+ This **must** be manually checked and `userStore.reset()` should be called if the check fails.
292
+
293
+ ```js
294
+ // This example is specifically for type 'Settings'
295
+ if (
296
+ result &&
297
+ typeof result === 'object' &&
298
+ 'theme' in result &&
299
+ 'language' in result &&
300
+ (result.theme === 'system' ||
301
+ result.theme === 'light' ||
302
+ result.theme === 'dark') &&
303
+ typeof result.language === 'string'
304
+ ) {
305
+ // 'result' is of type 'T'
306
+ } else {
307
+ // 'result' is not of type 'T'
308
+ userStore.reset()
309
+ console.log(userStore.value) // {theme: 'system', language: 'en'}
310
+ }
311
+
312
+ // 'userStore.value' is of type 'T'
255
313
  ```
256
314
 
257
315
  <br>
package/dist/index.cjs CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  class HyperStorage {
11
11
  /** Version of the library, injected via Rollup replace plugin. */
12
- static version = "5.0.6";
12
+ static version = "5.1.0";
13
13
  /** Key name under which the data is stored. */
14
14
  itemName;
15
15
  /** Default value used when the key does not exist in storage. */
@@ -26,7 +26,7 @@ class HyperStorage {
26
26
  * Creates a new HyperStorage instance.
27
27
  *
28
28
  * @param {string} itemName - The key name under which the data will be stored.
29
- * @param {T} [defaultValue] - Default value if the key does not exist.
29
+ * @param {T} [defaultValue] - Default value assigned to the key if it does not exist yet.
30
30
  * @param {Object} [options={}] - Optional configuration parameters.
31
31
  * @param {(value: string) => string} [options.encodeFn] - Optional function to encode stored values.
32
32
  * @param {(value: string) => string} [options.decodeFn] - Optional function to decode stored values.
@@ -69,11 +69,14 @@ class HyperStorage {
69
69
  stringValue = value;
70
70
  }
71
71
  else if (value === undefined ||
72
- (typeof value === 'number' && (isNaN(value) || value === Infinity || value === -Infinity)))
72
+ (typeof value === 'number' &&
73
+ (isNaN(value) || value === Infinity || value === -Infinity))) {
73
74
  // Manually stringify non-JSON values
74
- stringValue = String(value);
75
- else
75
+ stringValue = '\0' + String(value);
76
+ }
77
+ else {
76
78
  stringValue = '\0' + JSON.stringify(value);
79
+ }
77
80
  this.storage.setItem(this.itemName, this.encodeFn(stringValue));
78
81
  }
79
82
  /**
@@ -104,8 +107,9 @@ class HyperStorage {
104
107
  value = decodeFn(value);
105
108
  }
106
109
  catch (err) {
110
+ this.reset();
107
111
  console.error(err);
108
- return this.reset();
112
+ return err;
109
113
  }
110
114
  if (value[0] !== '\0')
111
115
  return (this.value = value); // Raw string value
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ declare class HyperStorage<T> {
23
23
  * Creates a new HyperStorage instance.
24
24
  *
25
25
  * @param {string} itemName - The key name under which the data will be stored.
26
- * @param {T} [defaultValue] - Default value if the key does not exist.
26
+ * @param {T} [defaultValue] - Default value assigned to the key if it does not exist yet.
27
27
  * @param {Object} [options={}] - Optional configuration parameters.
28
28
  * @param {(value: string) => string} [options.encodeFn] - Optional function to encode stored values.
29
29
  * @param {(value: string) => string} [options.decodeFn] - Optional function to decode stored values.
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  class HyperStorage {
9
9
  /** Version of the library, injected via Rollup replace plugin. */
10
- static version = "5.0.6";
10
+ static version = "5.1.0";
11
11
  /** Key name under which the data is stored. */
12
12
  itemName;
13
13
  /** Default value used when the key does not exist in storage. */
@@ -24,7 +24,7 @@ class HyperStorage {
24
24
  * Creates a new HyperStorage instance.
25
25
  *
26
26
  * @param {string} itemName - The key name under which the data will be stored.
27
- * @param {T} [defaultValue] - Default value if the key does not exist.
27
+ * @param {T} [defaultValue] - Default value assigned to the key if it does not exist yet.
28
28
  * @param {Object} [options={}] - Optional configuration parameters.
29
29
  * @param {(value: string) => string} [options.encodeFn] - Optional function to encode stored values.
30
30
  * @param {(value: string) => string} [options.decodeFn] - Optional function to decode stored values.
@@ -67,11 +67,14 @@ class HyperStorage {
67
67
  stringValue = value;
68
68
  }
69
69
  else if (value === undefined ||
70
- (typeof value === 'number' && (isNaN(value) || value === Infinity || value === -Infinity)))
70
+ (typeof value === 'number' &&
71
+ (isNaN(value) || value === Infinity || value === -Infinity))) {
71
72
  // Manually stringify non-JSON values
72
- stringValue = String(value);
73
- else
73
+ stringValue = '\0' + String(value);
74
+ }
75
+ else {
74
76
  stringValue = '\0' + JSON.stringify(value);
77
+ }
75
78
  this.storage.setItem(this.itemName, this.encodeFn(stringValue));
76
79
  }
77
80
  /**
@@ -102,8 +105,9 @@ class HyperStorage {
102
105
  value = decodeFn(value);
103
106
  }
104
107
  catch (err) {
108
+ this.reset();
105
109
  console.error(err);
106
- return this.reset();
110
+ return err;
107
111
  }
108
112
  if (value[0] !== '\0')
109
113
  return (this.value = value); // Raw string value
package/dist/index.umd.js CHANGED
@@ -1 +1 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).StorageManager=t()}(this,function(){"use strict";return class{static version="5.0.6";itemName;defaultValue;encodeFn;decodeFn;storage;#e;constructor(e,t,i={}){const{encodeFn:n,decodeFn:s,storage:o=window.localStorage}=i;if("string"!=typeof e)throw new TypeError("itemName is not a string");if(this.itemName=e,this.defaultValue=t,n&&"function"!=typeof n)throw new TypeError("encodeFn is defined but is not a function");if(this.encodeFn=n||(e=>e),s&&"function"!=typeof s)throw new TypeError("decodeFn is defined but is not a function");if(this.decodeFn=s||(e=>e),!(o instanceof Storage))throw new TypeError("storage must be an instance of Storage");this.storage=o,this.sync()}set value(e){let t;this.#e=e,t="string"==typeof e?"\0"===e[0]?"\0"+e:e:void 0===e||"number"==typeof e&&(isNaN(e)||e===1/0||e===-1/0)?String(e):"\0"+JSON.stringify(e),this.storage.setItem(this.itemName,this.encodeFn(t))}get value(){return this.#e??this.defaultValue}set(e){return this.value=e(this.value)}sync(e=this.decodeFn){let t=this.storage.getItem(this.itemName);if("string"!=typeof t)return this.reset();try{t=e(t)}catch(e){return console.error(e),this.reset()}return"\0"!==t[0]?this.value=t:(t=t.slice(1),"\0"===t[0]?this.value=t:this.value="undefined"===t?void 0:"NaN"===t?NaN:"Infinity"===t?1/0:"-Infinity"===t?-1/0:JSON.parse(t))}reset(){return this.value=this.defaultValue}remove(){this.#e=void 0,this.storage.removeItem(this.itemName)}clear(){this.#e=void 0,this.storage.clear()}isDefault(){return this.#e===this.defaultValue}}});
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).HyperStorage=t()}(this,function(){"use strict";return class{static version="5.1.0";itemName;defaultValue;encodeFn;decodeFn;storage;#e;constructor(e,t,i={}){const{encodeFn:s,decodeFn:n,storage:o=window.localStorage}=i;if("string"!=typeof e)throw new TypeError("itemName is not a string");if(this.itemName=e,this.defaultValue=t,s&&"function"!=typeof s)throw new TypeError("encodeFn is defined but is not a function");if(this.encodeFn=s||(e=>e),n&&"function"!=typeof n)throw new TypeError("decodeFn is defined but is not a function");if(this.decodeFn=n||(e=>e),!(o instanceof Storage))throw new TypeError("storage must be an instance of Storage");this.storage=o,this.sync()}set value(e){let t;this.#e=e,t="string"==typeof e?"\0"===e[0]?"\0"+e:e:void 0===e||"number"==typeof e&&(isNaN(e)||e===1/0||e===-1/0)?"\0"+String(e):"\0"+JSON.stringify(e),this.storage.setItem(this.itemName,this.encodeFn(t))}get value(){return this.#e??this.defaultValue}set(e){return this.value=e(this.value)}sync(e=this.decodeFn){let t=this.storage.getItem(this.itemName);if("string"!=typeof t)return this.reset();try{t=e(t)}catch(e){return this.reset(),console.error(e),e}return"\0"!==t[0]?this.value=t:(t=t.slice(1),"\0"===t[0]?this.value=t:this.value="undefined"===t?void 0:"NaN"===t?NaN:"Infinity"===t?1/0:"-Infinity"===t?-1/0:JSON.parse(t))}reset(){return this.value=this.defaultValue}remove(){this.#e=void 0,this.storage.removeItem(this.itemName)}clear(){this.#e=void 0,this.storage.clear()}isDefault(){return this.#e===this.defaultValue}}});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperstorage-js",
3
- "version": "5.0.6",
3
+ "version": "5.1.0",
4
4
  "description": "A lightweight wrapper for localStorage/sessionStorage with efficient caching and type-preserving serialization.",
5
5
  "license": "MIT",
6
6
  "author": "Khoeckman",
@@ -34,7 +34,7 @@
34
34
  "prettier": "^3.7.4",
35
35
  "rollup": "^4.55.1",
36
36
  "rollup-plugin-delete": "^3.0.2",
37
- "rollup-plugin-prettier": "^4.1.2",
37
+ "rollup-plugin-prettier": "^4.1.1",
38
38
  "tslib": "^2.8.1",
39
39
  "typescript": "^5.9.3"
40
40
  },
package/src/index.ts CHANGED
@@ -34,7 +34,7 @@ class HyperStorage<T> {
34
34
  * Creates a new HyperStorage instance.
35
35
  *
36
36
  * @param {string} itemName - The key name under which the data will be stored.
37
- * @param {T} [defaultValue] - Default value if the key does not exist.
37
+ * @param {T} [defaultValue] - Default value assigned to the key if it does not exist yet.
38
38
  * @param {Object} [options={}] - Optional configuration parameters.
39
39
  * @param {(value: string) => string} [options.encodeFn] - Optional function to encode stored values.
40
40
  * @param {(value: string) => string} [options.decodeFn] - Optional function to decode stored values.
@@ -55,17 +55,21 @@ class HyperStorage<T> {
55
55
  ) {
56
56
  const { encodeFn, decodeFn, storage = window.localStorage } = options
57
57
 
58
- if (typeof itemName !== 'string') throw new TypeError('itemName is not a string')
58
+ if (typeof itemName !== 'string')
59
+ throw new TypeError('itemName is not a string')
59
60
  this.itemName = itemName
60
61
  this.defaultValue = defaultValue
61
62
 
62
- if (encodeFn && typeof encodeFn !== 'function') throw new TypeError('encodeFn is defined but is not a function')
63
+ if (encodeFn && typeof encodeFn !== 'function')
64
+ throw new TypeError('encodeFn is defined but is not a function')
63
65
  this.encodeFn = encodeFn || ((v) => v)
64
66
 
65
- if (decodeFn && typeof decodeFn !== 'function') throw new TypeError('decodeFn is defined but is not a function')
67
+ if (decodeFn && typeof decodeFn !== 'function')
68
+ throw new TypeError('decodeFn is defined but is not a function')
66
69
  this.decodeFn = decodeFn || ((v) => v)
67
70
 
68
- if (!(storage instanceof Storage)) throw new TypeError('storage must be an instance of Storage')
71
+ if (!(storage instanceof Storage))
72
+ throw new TypeError('storage must be an instance of Storage')
69
73
  this.storage = storage
70
74
 
71
75
  this.sync()
@@ -87,11 +91,15 @@ class HyperStorage<T> {
87
91
  else stringValue = value
88
92
  } else if (
89
93
  value === undefined ||
90
- (typeof value === 'number' && (isNaN(value) || value === Infinity || value === -Infinity))
91
- )
94
+ (typeof value === 'number' &&
95
+ (isNaN(value) || value === Infinity || value === -Infinity))
96
+ ) {
92
97
  // Manually stringify non-JSON values
93
- stringValue = String(value)
94
- else stringValue = '\0' + JSON.stringify(value)
98
+ stringValue = '\0' + String(value)
99
+ } else {
100
+ stringValue = '\0' + JSON.stringify(value)
101
+ }
102
+
95
103
  this.storage.setItem(this.itemName, this.encodeFn(stringValue))
96
104
  }
97
105
 
@@ -125,8 +133,9 @@ class HyperStorage<T> {
125
133
  try {
126
134
  value = decodeFn(value)
127
135
  } catch (err) {
136
+ this.reset()
128
137
  console.error(err)
129
- return this.reset()
138
+ return err
130
139
  }
131
140
 
132
141
  if (value[0] !== '\0') return (this.value = value as T) // Raw string value