@david200197/super-ls 1.0.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/index.js ADDED
@@ -0,0 +1,615 @@
1
+ import { stringify, parse } from 'devalue';
2
+
3
+ /**
4
+ * @fileoverview SuperLocalStorage - Enhanced localStorage wrapper for Titan Planet
5
+ * that supports complex JavaScript types including Map, Set, Date, circular references,
6
+ * and custom class instances with automatic serialization/deserialization.
7
+ *
8
+ * @author Titan Planet
9
+ * @license MIT
10
+ */
11
+
12
+ // ============================================================================
13
+ // Constants
14
+ // ============================================================================
15
+
16
+ /** @constant {string} Default prefix for all storage keys */
17
+ const DEFAULT_PREFIX = 'sls_';
18
+
19
+ /** @constant {string} Metadata key for identifying serialized class type */
20
+ const TYPE_MARKER = '__super_type__';
21
+
22
+ /** @constant {string} Metadata key for serialized class data */
23
+ const DATA_MARKER = '__data__';
24
+
25
+ // ============================================================================
26
+ // Type Definitions
27
+ // ============================================================================
28
+
29
+ /**
30
+ * @typedef {Object} SerializedClassWrapper
31
+ * @property {string} __super_type__ - The registered type name of the class
32
+ * @property {Object} __data__ - The serialized properties of the class instance
33
+ */
34
+
35
+ /**
36
+ * @typedef {new (...args: any[]) => any} ClassConstructor
37
+ * A class constructor function
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} HydratableClass
42
+ * @property {function(Object): any} [hydrate] - Optional static method to create instance from data
43
+ */
44
+
45
+ // ============================================================================
46
+ // Helper Functions
47
+ // ============================================================================
48
+
49
+ /**
50
+ * Checks if a value is a primitive (non-object) type
51
+ * @param {any} value - Value to check
52
+ * @returns {boolean} True if value is null, undefined, or a primitive
53
+ */
54
+ const isPrimitive = (value) => value === null || typeof value !== 'object';
55
+
56
+ /**
57
+ * Checks if a value is a TypedArray (Uint8Array, Float32Array, etc.)
58
+ * @param {any} value - Value to check
59
+ * @returns {boolean} True if value is a TypedArray
60
+ */
61
+ const isTypedArray = (value) => ArrayBuffer.isView(value) && !(value instanceof DataView);
62
+
63
+ /**
64
+ * Checks if a serialized object contains class type metadata
65
+ * @param {any} value - Value to check
66
+ * @returns {boolean} True if value has type wrapper markers
67
+ */
68
+ const hasTypeWrapper = (value) => value[TYPE_MARKER] && value[DATA_MARKER] !== undefined;
69
+
70
+ // ============================================================================
71
+ // Main Class
72
+ // ============================================================================
73
+
74
+ /**
75
+ * Enhanced localStorage wrapper that supports complex JavaScript types
76
+ * and custom class serialization/deserialization.
77
+ *
78
+ * @class SuperLocalStorage
79
+ *
80
+ * @example
81
+ * // Basic usage with rich types
82
+ * import superLs from 'super-ls';
83
+ *
84
+ * const settings = new Map([['theme', 'dark'], ['lang', 'es']]);
85
+ * superLs.set('user_settings', settings);
86
+ *
87
+ * const recovered = superLs.get('user_settings');
88
+ * console.log(recovered.get('theme')); // 'dark'
89
+ *
90
+ * @example
91
+ * // Usage with custom classes
92
+ * class Player {
93
+ * constructor(name = '', score = 0) {
94
+ * this.name = name;
95
+ * this.score = score;
96
+ * }
97
+ * addScore(points) {
98
+ * this.score += points;
99
+ * }
100
+ * }
101
+ *
102
+ * superLs.register(Player);
103
+ * superLs.set('player', new Player('Alice', 100));
104
+ *
105
+ * const player = superLs.get('player');
106
+ * player.addScore(50); // Methods work!
107
+ */
108
+ export class SuperLocalStorage {
109
+ /**
110
+ * Creates a new SuperLocalStorage instance
111
+ * @param {string} [prefix='sls_'] - Prefix for all storage keys
112
+ */
113
+ constructor(prefix = DEFAULT_PREFIX) {
114
+ /**
115
+ * Registry mapping type names to class constructors
116
+ * @type {Map<string, ClassConstructor>}
117
+ * @private
118
+ */
119
+ this.registry = new Map();
120
+
121
+ /**
122
+ * Prefix prepended to all storage keys
123
+ * @type {string}
124
+ * @private
125
+ */
126
+ this.prefix = prefix;
127
+ }
128
+
129
+ // ========================================================================
130
+ // Public API
131
+ // ========================================================================
132
+
133
+ /**
134
+ * Registers a class for serialization/deserialization support.
135
+ *
136
+ * Once registered, instances of this class can be stored and retrieved
137
+ * with their methods intact.
138
+ *
139
+ * @param {ClassConstructor & HydratableClass} ClassRef - The class constructor to register
140
+ * @param {string} [typeName=null] - Optional custom type name (defaults to class name)
141
+ * @throws {Error} If ClassRef is not a function/class
142
+ *
143
+ * @example
144
+ * // Basic registration
145
+ * superLs.register(Player);
146
+ *
147
+ * @example
148
+ * // Registration with custom name (useful for minified code or name collisions)
149
+ * superLs.register(Player, 'GamePlayer');
150
+ *
151
+ * @example
152
+ * // Class with static hydrate method for complex constructors
153
+ * class Player {
154
+ * constructor(name, score) {
155
+ * if (!name) throw new Error('Name required');
156
+ * this.name = name;
157
+ * this.score = score;
158
+ * }
159
+ * static hydrate(data) {
160
+ * return new Player(data.name, data.score);
161
+ * }
162
+ * }
163
+ * superLs.register(Player);
164
+ */
165
+ register(ClassRef, typeName = null) {
166
+ if (typeof ClassRef !== 'function') {
167
+ throw new Error('Invalid class: expected a constructor function');
168
+ }
169
+
170
+ const finalName = typeName || ClassRef.name;
171
+ this.registry.set(finalName, ClassRef);
172
+ }
173
+
174
+ /**
175
+ * Stores a value in localStorage with full type preservation.
176
+ *
177
+ * Supports: primitives, objects, arrays, Map, Set, Date, RegExp,
178
+ * TypedArrays, BigInt, circular references, undefined, NaN, Infinity,
179
+ * and registered class instances.
180
+ *
181
+ * @param {string} key - Storage key
182
+ * @param {any} value - Value to store
183
+ * @throws {Error} If value contains non-serializable types (functions, WeakMap, WeakSet)
184
+ *
185
+ * @example
186
+ * // Store various types
187
+ * superLs.set('map', new Map([['key', 'value']]));
188
+ * superLs.set('set', new Set([1, 2, 3]));
189
+ * superLs.set('date', new Date());
190
+ * superLs.set('regex', /pattern/gi);
191
+ * superLs.set('bigint', BigInt('9007199254740991000'));
192
+ *
193
+ * @example
194
+ * // Store circular references
195
+ * const obj = { name: 'circular' };
196
+ * obj.self = obj;
197
+ * superLs.set('circular', obj);
198
+ */
199
+ set(key, value) {
200
+ const payload = this._toSerializable(value);
201
+ const serialized = stringify(payload);
202
+ t.ls.set(this.prefix + key, serialized);
203
+ }
204
+
205
+ /**
206
+ * Retrieves a value from localStorage with full type restoration.
207
+ *
208
+ * All types are automatically restored to their original form,
209
+ * including registered class instances with working methods.
210
+ *
211
+ * @param {string} key - Storage key
212
+ * @returns {any} The stored value with types restored, or null if key doesn't exist
213
+ *
214
+ * @example
215
+ * const settings = superLs.get('user_settings');
216
+ * if (settings) {
217
+ * console.log(settings.get('theme')); // Map methods work
218
+ * }
219
+ */
220
+ get(key) {
221
+ const raw = t.ls.get(this.prefix + key);
222
+
223
+ if (!raw) {
224
+ return null;
225
+ }
226
+
227
+ const parsed = parse(raw);
228
+ return this._rehydrate(parsed, new WeakMap());
229
+ }
230
+
231
+ // ========================================================================
232
+ // Private Methods - Serialization
233
+ // ========================================================================
234
+
235
+ /**
236
+ * Recursively converts values to a serializable format.
237
+ *
238
+ * Registered class instances are wrapped with type metadata.
239
+ * Circular references are preserved using a WeakMap tracker.
240
+ *
241
+ * @param {any} value - Value to convert
242
+ * @param {WeakMap} [seen=new WeakMap()] - Tracks processed objects for circular reference handling
243
+ * @returns {any} Serializable representation of the value
244
+ * @private
245
+ */
246
+ _toSerializable(value, seen = new WeakMap()) {
247
+ if (isPrimitive(value)) {
248
+ return value;
249
+ }
250
+
251
+ if (seen.has(value)) {
252
+ return seen.get(value);
253
+ }
254
+
255
+ // Check registered classes first (before native types)
256
+ const classWrapper = this._tryWrapRegisteredClass(value, seen);
257
+ if (classWrapper) {
258
+ return classWrapper;
259
+ }
260
+
261
+ // Handle native types that devalue supports
262
+ if (this._isNativelySerializable(value)) {
263
+ return value;
264
+ }
265
+
266
+ // Handle collections and objects
267
+ return this._serializeCollection(value, seen);
268
+ }
269
+
270
+ /**
271
+ * Attempts to wrap a registered class instance with type metadata
272
+ * @param {any} value - Value to check and potentially wrap
273
+ * @param {WeakMap} seen - Circular reference tracker
274
+ * @returns {SerializedClassWrapper|null} Wrapped class or null if not a registered class
275
+ * @private
276
+ */
277
+ _tryWrapRegisteredClass(value, seen) {
278
+ for (const [name, Constructor] of this.registry.entries()) {
279
+ if (value instanceof Constructor) {
280
+ const wrapper = {
281
+ [TYPE_MARKER]: name,
282
+ [DATA_MARKER]: {}
283
+ };
284
+
285
+ seen.set(value, wrapper);
286
+
287
+ for (const key of Object.keys(value)) {
288
+ wrapper[DATA_MARKER][key] = this._toSerializable(value[key], seen);
289
+ }
290
+
291
+ return wrapper;
292
+ }
293
+ }
294
+ return null;
295
+ }
296
+
297
+ /**
298
+ * Checks if a value is natively serializable by devalue
299
+ * @param {any} value - Value to check
300
+ * @returns {boolean} True if devalue handles this type natively
301
+ * @private
302
+ */
303
+ _isNativelySerializable(value) {
304
+ return value instanceof Date ||
305
+ value instanceof RegExp ||
306
+ isTypedArray(value);
307
+ }
308
+
309
+ /**
310
+ * Serializes collections (Array, Map, Set, Object)
311
+ * @param {any} value - Collection to serialize
312
+ * @param {WeakMap} seen - Circular reference tracker
313
+ * @returns {any} Serialized collection
314
+ * @private
315
+ */
316
+ _serializeCollection(value, seen) {
317
+ if (Array.isArray(value)) {
318
+ return this._serializeArray(value, seen);
319
+ }
320
+
321
+ if (value instanceof Map) {
322
+ return this._serializeMap(value, seen);
323
+ }
324
+
325
+ if (value instanceof Set) {
326
+ return this._serializeSet(value, seen);
327
+ }
328
+
329
+ return this._serializeObject(value, seen);
330
+ }
331
+
332
+ /**
333
+ * Serializes an array, preserving sparse array holes
334
+ * @param {Array} value - Array to serialize
335
+ * @param {WeakMap} seen - Circular reference tracker
336
+ * @returns {Array} Serialized array
337
+ * @private
338
+ */
339
+ _serializeArray(value, seen) {
340
+ const arr = [];
341
+ seen.set(value, arr);
342
+
343
+ for (let i = 0; i < value.length; i++) {
344
+ if (i in value) {
345
+ arr[i] = this._toSerializable(value[i], seen);
346
+ }
347
+ }
348
+
349
+ if (value.length > arr.length) {
350
+ arr.length = value.length;
351
+ }
352
+
353
+ return arr;
354
+ }
355
+
356
+ /**
357
+ * Serializes a Map, processing both keys and values
358
+ * @param {Map} value - Map to serialize
359
+ * @param {WeakMap} seen - Circular reference tracker
360
+ * @returns {Map} Serialized Map
361
+ * @private
362
+ */
363
+ _serializeMap(value, seen) {
364
+ const newMap = new Map();
365
+ seen.set(value, newMap);
366
+
367
+ for (const [k, v] of value.entries()) {
368
+ newMap.set(
369
+ this._toSerializable(k, seen),
370
+ this._toSerializable(v, seen)
371
+ );
372
+ }
373
+
374
+ return newMap;
375
+ }
376
+
377
+ /**
378
+ * Serializes a Set
379
+ * @param {Set} value - Set to serialize
380
+ * @param {WeakMap} seen - Circular reference tracker
381
+ * @returns {Set} Serialized Set
382
+ * @private
383
+ */
384
+ _serializeSet(value, seen) {
385
+ const newSet = new Set();
386
+ seen.set(value, newSet);
387
+
388
+ for (const item of value) {
389
+ newSet.add(this._toSerializable(item, seen));
390
+ }
391
+
392
+ return newSet;
393
+ }
394
+
395
+ /**
396
+ * Serializes a plain object or unregistered class instance
397
+ * @param {Object} value - Object to serialize
398
+ * @param {WeakMap} seen - Circular reference tracker
399
+ * @returns {Object} Serialized object
400
+ * @private
401
+ */
402
+ _serializeObject(value, seen) {
403
+ const obj = {};
404
+ seen.set(value, obj);
405
+
406
+ for (const key of Object.keys(value)) {
407
+ obj[key] = this._toSerializable(value[key], seen);
408
+ }
409
+
410
+ return obj;
411
+ }
412
+
413
+ // ========================================================================
414
+ // Private Methods - Deserialization (Rehydration)
415
+ // ========================================================================
416
+
417
+ /**
418
+ * Recursively rehydrates serialized data back to original types.
419
+ *
420
+ * Objects with type metadata are restored to class instances.
421
+ * Circular references are preserved using a WeakMap tracker.
422
+ *
423
+ * @param {any} value - Value to rehydrate
424
+ * @param {WeakMap} seen - Tracks processed objects for circular reference handling
425
+ * @returns {any} Rehydrated value with original types restored
426
+ * @private
427
+ */
428
+ _rehydrate(value, seen) {
429
+ if (isPrimitive(value)) {
430
+ return value;
431
+ }
432
+
433
+ if (seen.has(value)) {
434
+ return seen.get(value);
435
+ }
436
+
437
+ // Check for wrapped class instances
438
+ if (hasTypeWrapper(value)) {
439
+ return this._rehydrateClass(value, seen);
440
+ }
441
+
442
+ // Handle native types
443
+ if (value instanceof Date || value instanceof RegExp) {
444
+ return value;
445
+ }
446
+
447
+ // Handle collections
448
+ return this._rehydrateCollection(value, seen);
449
+ }
450
+
451
+ /**
452
+ * Rehydrates a wrapped class instance back to its original class
453
+ * @param {SerializedClassWrapper} value - Wrapped class data
454
+ * @param {WeakMap} seen - Circular reference tracker
455
+ * @returns {any} Restored class instance or original value if class not registered
456
+ * @private
457
+ */
458
+ _rehydrateClass(value, seen) {
459
+ const Constructor = this.registry.get(value[TYPE_MARKER]);
460
+
461
+ if (!Constructor) {
462
+ return this._rehydrateObject(value, seen);
463
+ }
464
+
465
+ // Use placeholder for circular reference support
466
+ const placeholder = {};
467
+ seen.set(value, placeholder);
468
+
469
+ // Rehydrate nested data first
470
+ const hydratedData = {};
471
+ for (const key of Object.keys(value[DATA_MARKER])) {
472
+ hydratedData[key] = this._rehydrate(value[DATA_MARKER][key], seen);
473
+ }
474
+
475
+ // Create instance using hydrate() or default constructor
476
+ const instance = this._createInstance(Constructor, hydratedData);
477
+
478
+ // Update placeholder to become the actual instance
479
+ Object.assign(placeholder, instance);
480
+ Object.setPrototypeOf(placeholder, Object.getPrototypeOf(instance));
481
+
482
+ return placeholder;
483
+ }
484
+
485
+ /**
486
+ * Creates a class instance from hydrated data
487
+ * @param {ClassConstructor & HydratableClass} Constructor - Class constructor
488
+ * @param {Object} data - Hydrated property data
489
+ * @returns {any} New class instance
490
+ * @private
491
+ */
492
+ _createInstance(Constructor, data) {
493
+ if (typeof Constructor.hydrate === 'function') {
494
+ return Constructor.hydrate(data);
495
+ }
496
+
497
+ const instance = new Constructor();
498
+ Object.assign(instance, data);
499
+ return instance;
500
+ }
501
+
502
+ /**
503
+ * Rehydrates collections (Array, Map, Set, Object)
504
+ * @param {any} value - Collection to rehydrate
505
+ * @param {WeakMap} seen - Circular reference tracker
506
+ * @returns {any} Rehydrated collection
507
+ * @private
508
+ */
509
+ _rehydrateCollection(value, seen) {
510
+ if (Array.isArray(value)) {
511
+ return this._rehydrateArray(value, seen);
512
+ }
513
+
514
+ if (value instanceof Map) {
515
+ return this._rehydrateMap(value, seen);
516
+ }
517
+
518
+ if (value instanceof Set) {
519
+ return this._rehydrateSet(value, seen);
520
+ }
521
+
522
+ if (value.constructor === Object) {
523
+ return this._rehydrateObject(value, seen);
524
+ }
525
+
526
+ return value;
527
+ }
528
+
529
+ /**
530
+ * Rehydrates an array
531
+ * @param {Array} value - Array to rehydrate
532
+ * @param {WeakMap} seen - Circular reference tracker
533
+ * @returns {Array} Rehydrated array
534
+ * @private
535
+ */
536
+ _rehydrateArray(value, seen) {
537
+ const arr = [];
538
+ seen.set(value, arr);
539
+
540
+ for (let i = 0; i < value.length; i++) {
541
+ arr[i] = this._rehydrate(value[i], seen);
542
+ }
543
+
544
+ return arr;
545
+ }
546
+
547
+ /**
548
+ * Rehydrates a Map, processing both keys and values
549
+ * @param {Map} value - Map to rehydrate
550
+ * @param {WeakMap} seen - Circular reference tracker
551
+ * @returns {Map} Rehydrated Map
552
+ * @private
553
+ */
554
+ _rehydrateMap(value, seen) {
555
+ const newMap = new Map();
556
+ seen.set(value, newMap);
557
+
558
+ for (const [k, v] of value.entries()) {
559
+ newMap.set(
560
+ this._rehydrate(k, seen),
561
+ this._rehydrate(v, seen)
562
+ );
563
+ }
564
+
565
+ return newMap;
566
+ }
567
+
568
+ /**
569
+ * Rehydrates a Set
570
+ * @param {Set} value - Set to rehydrate
571
+ * @param {WeakMap} seen - Circular reference tracker
572
+ * @returns {Set} Rehydrated Set
573
+ * @private
574
+ */
575
+ _rehydrateSet(value, seen) {
576
+ const newSet = new Set();
577
+ seen.set(value, newSet);
578
+
579
+ for (const item of value) {
580
+ newSet.add(this._rehydrate(item, seen));
581
+ }
582
+
583
+ return newSet;
584
+ }
585
+
586
+ /**
587
+ * Rehydrates a plain object
588
+ * @param {Object} value - Object to rehydrate
589
+ * @param {WeakMap} seen - Circular reference tracker
590
+ * @returns {Object} Rehydrated object
591
+ * @private
592
+ */
593
+ _rehydrateObject(value, seen) {
594
+ const obj = {};
595
+ seen.set(value, obj);
596
+
597
+ for (const key of Object.keys(value)) {
598
+ obj[key] = this._rehydrate(value[key], seen);
599
+ }
600
+
601
+ return obj;
602
+ }
603
+ }
604
+
605
+ // ============================================================================
606
+ // Default Export
607
+ // ============================================================================
608
+
609
+ /**
610
+ * Default SuperLocalStorage instance for convenient usage
611
+ * @type {SuperLocalStorage}
612
+ */
613
+ const superLs = new SuperLocalStorage();
614
+
615
+ export default superLs;
package/jsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "es2021",
5
+ "checkJs": false,
6
+ "allowJs": true,
7
+ "moduleResolution": "node"
8
+ },
9
+ "include": [
10
+ "index.js",
11
+ "node_modules/titan-sdk/index.d.ts"
12
+ ]
13
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "src": ".",
3
+ "ignore": "mkctx.config.json, pnpm-lock.yaml, **/.titan/, mkctx/, node_modules/, .git/, dist/, build/, target/, .next/, out/, .cache, package-lock.json, *.log, temp/, tmp/, coverage/, .nyc_output, .env, .env.local, .env.development.local, .env.test.local, .env.production.local, npm-debug.log*, yarn-debug.log*, yarn-error.log*, .npm, .yarn-integrity, .parcel-cache, .vuepress/dist, .svelte-kit, **/*.rs.bk, .idea/, .vscode/, .DS_Store, Thumbs.db, *.swp, *.swo, .~lock.*, Cargo.lock, .cargo/registry/, .cargo/git/, .rustup/, *.pdb, *.dSYM/, *.so, *.dll, *.dylib, *.exe, *.lib, *.a, *.o, *.rlib, *.d, *.tmp, *.bak, *.orig, *.rej, *.pyc, *.pyo, *.class, *.jar, *.war, *.ear, *.zip, *.tar.gz, *.rar, *.7z, *.iso, *.img, *.dmg, *.pdf, *.doc, *.docx, *.xls, *.xlsx, *.ppt, *.pptx",
4
+ "output": "./mkctx",
5
+ "first_comment": "/* Project Context */",
6
+ "last_comment": "/* End of Context */"
7
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@david200197/super-ls",
3
+ "version": "1.0.0",
4
+ "description": "A supercharged storage adapter for Titan Planet that enables storing complex objects, circular references, and Class instances with automatic rehydration.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "vitest run",
9
+ "test:watch": "vitest",
10
+ "test:cov": "vitest --coverage"
11
+ },
12
+ "keywords": [
13
+ "titan planet",
14
+ "extension"
15
+ ],
16
+ "author": "",
17
+ "license": "ISC",
18
+ "dependencies": {
19
+ "devalue": "^5.6.2"
20
+ },
21
+ "peerDependencies": {
22
+ "@titanpl/core": ">=1.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@titanpl/core": "latest",
26
+ "@vitest/coverage-v8": "^4.0.17",
27
+ "esbuild": "^0.27.2",
28
+ "titanpl-sdk": "latest",
29
+ "vitest": "^4.0.17"
30
+ }
31
+ }