titanpl-superls 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.
@@ -0,0 +1,911 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { SuperLocalStorage } from "./index.js";
3
+
4
+ // ============================================
5
+ // MOCK for Titan Planet's t.ls API
6
+ // ============================================
7
+ const mockStorage = new Map();
8
+
9
+ globalThis.t = {
10
+ ls: {
11
+ set(key, value) { mockStorage.set(key, value); },
12
+ get(key) { return mockStorage.get(key) || null; },
13
+ remove(key) { mockStorage.delete(key); },
14
+ clear() { mockStorage.clear(); }
15
+ }
16
+ };
17
+
18
+ // ============================================
19
+ // EDGE CASE TESTS
20
+ // ============================================
21
+ describe('SuperLocalStorage - Edge Cases', () => {
22
+ let superLs;
23
+
24
+ beforeEach(() => {
25
+ mockStorage.clear();
26
+ superLs = new SuperLocalStorage();
27
+ });
28
+
29
+ // ==========================================
30
+ // CLASS INHERITANCE
31
+ // ==========================================
32
+ describe('Class Inheritance', () => {
33
+ it('should handle simple class inheritance', () => {
34
+ class Animal {
35
+ constructor(name = '') {
36
+ this.name = name;
37
+ }
38
+ speak() {
39
+ return `${this.name} makes a sound`;
40
+ }
41
+ }
42
+
43
+ class Dog extends Animal {
44
+ constructor(name = '', breed = '') {
45
+ super(name);
46
+ this.breed = breed;
47
+ }
48
+ speak() {
49
+ return `${this.name} barks!`;
50
+ }
51
+ fetch() {
52
+ return `${this.name} fetches the ball`;
53
+ }
54
+ }
55
+
56
+ superLs.register(Dog);
57
+
58
+ const dog = new Dog('Rex', 'German Shepherd');
59
+ superLs.set('dog', dog);
60
+ const recovered = superLs.get('dog');
61
+
62
+ expect(recovered).toBeInstanceOf(Dog);
63
+ expect(recovered).toBeInstanceOf(Animal);
64
+ expect(recovered.name).toBe('Rex');
65
+ expect(recovered.breed).toBe('German Shepherd');
66
+ expect(recovered.speak()).toBe('Rex barks!');
67
+ expect(recovered.fetch()).toBe('Rex fetches the ball');
68
+ });
69
+
70
+ it('should handle multi-level inheritance', () => {
71
+ class Vehicle {
72
+ constructor() { this.wheels = 0; }
73
+ getWheels() { return this.wheels; }
74
+ }
75
+
76
+ class Car extends Vehicle {
77
+ constructor() {
78
+ super();
79
+ this.wheels = 4;
80
+ this.doors = 4;
81
+ }
82
+ honk() { return 'Beep!'; }
83
+ }
84
+
85
+ class SportsCar extends Car {
86
+ constructor() {
87
+ super();
88
+ this.doors = 2;
89
+ this.topSpeed = 200;
90
+ }
91
+ race() { return `Racing at ${this.topSpeed} mph!`; }
92
+ }
93
+
94
+ superLs.register(SportsCar);
95
+
96
+ const car = new SportsCar();
97
+ superLs.set('sportscar', car);
98
+ const recovered = superLs.get('sportscar');
99
+
100
+ expect(recovered).toBeInstanceOf(SportsCar);
101
+ expect(recovered).toBeInstanceOf(Car);
102
+ expect(recovered).toBeInstanceOf(Vehicle);
103
+ expect(recovered.wheels).toBe(4);
104
+ expect(recovered.doors).toBe(2);
105
+ expect(recovered.topSpeed).toBe(200);
106
+ expect(recovered.honk()).toBe('Beep!');
107
+ expect(recovered.race()).toBe('Racing at 200 mph!');
108
+ });
109
+
110
+ it('should handle inheritance with dependency injection', () => {
111
+ class Engine {
112
+ constructor(hp = 0) { this.horsepower = hp; }
113
+ start() { return `Engine with ${this.horsepower}hp starting...`; }
114
+ }
115
+
116
+ class Vehicle {
117
+ constructor(engine = null) { this.engine = engine; }
118
+ }
119
+
120
+ class Car extends Vehicle {
121
+ constructor(engine = null, model = '') {
122
+ super(engine);
123
+ this.model = model;
124
+ }
125
+ drive() {
126
+ return `${this.model}: ${this.engine?.start() || 'No engine'}`;
127
+ }
128
+ }
129
+
130
+ superLs.register(Engine);
131
+ superLs.register(Car);
132
+
133
+ const engine = new Engine(300);
134
+ const car = new Car(engine, 'Mustang');
135
+ superLs.set('car', car);
136
+ const recovered = superLs.get('car');
137
+
138
+ expect(recovered).toBeInstanceOf(Car);
139
+ expect(recovered.engine).toBeInstanceOf(Engine);
140
+ expect(recovered.drive()).toBe('Mustang: Engine with 300hp starting...');
141
+ });
142
+ });
143
+
144
+ // ==========================================
145
+ // SHARED/DUPLICATE REFERENCES
146
+ // ==========================================
147
+ describe('Shared References', () => {
148
+ it('should handle same instance referenced multiple times', () => {
149
+ class Item {
150
+ constructor(name = '') { this.name = name; }
151
+ }
152
+
153
+ superLs.register(Item);
154
+
155
+ const sharedItem = new Item('Shared Sword');
156
+ const data = {
157
+ primary: sharedItem,
158
+ secondary: sharedItem,
159
+ backup: sharedItem
160
+ };
161
+
162
+ superLs.set('inventory', data);
163
+ const recovered = superLs.get('inventory');
164
+
165
+ expect(recovered.primary.name).toBe('Shared Sword');
166
+ expect(recovered.secondary.name).toBe('Shared Sword');
167
+ expect(recovered.backup.name).toBe('Shared Sword');
168
+
169
+ // All should reference the same object (deduplication)
170
+ expect(recovered.primary).toBe(recovered.secondary);
171
+ expect(recovered.secondary).toBe(recovered.backup);
172
+ });
173
+
174
+ it('should handle shared instance in nested structures', () => {
175
+ class Config {
176
+ constructor(value = '') { this.value = value; }
177
+ }
178
+
179
+ superLs.register(Config);
180
+
181
+ const sharedConfig = new Config('shared-setting');
182
+ const data = {
183
+ level1: {
184
+ level2: {
185
+ config: sharedConfig
186
+ },
187
+ alsoConfig: sharedConfig
188
+ },
189
+ rootConfig: sharedConfig
190
+ };
191
+
192
+ superLs.set('nested', data);
193
+ const recovered = superLs.get('nested');
194
+
195
+ expect(recovered.level1.level2.config).toBe(recovered.level1.alsoConfig);
196
+ expect(recovered.level1.alsoConfig).toBe(recovered.rootConfig);
197
+ });
198
+
199
+ it('should handle shared instance in array', () => {
200
+ class Token {
201
+ constructor(id = '') { this.id = id; }
202
+ }
203
+
204
+ superLs.register(Token);
205
+
206
+ const token = new Token('abc123');
207
+ const arr = [token, token, token];
208
+
209
+ superLs.set('tokens', arr);
210
+ const recovered = superLs.get('tokens');
211
+
212
+ expect(recovered[0]).toBe(recovered[1]);
213
+ expect(recovered[1]).toBe(recovered[2]);
214
+ });
215
+ });
216
+
217
+ // ==========================================
218
+ // UNREGISTERED CLASSES
219
+ // ==========================================
220
+ describe('Unregistered Classes as Dependencies', () => {
221
+ it('should convert unregistered class to plain object', () => {
222
+ class Registered {
223
+ constructor() { this.name = 'registered'; }
224
+ getName() { return this.name; }
225
+ }
226
+
227
+ class Unregistered {
228
+ constructor() { this.value = 42; }
229
+ getValue() { return this.value; }
230
+ }
231
+
232
+ superLs.register(Registered);
233
+ // Note: Unregistered is NOT registered
234
+
235
+ const reg = new Registered();
236
+ reg.dependency = new Unregistered();
237
+
238
+ superLs.set('mixed', reg);
239
+ const recovered = superLs.get('mixed');
240
+
241
+ expect(recovered).toBeInstanceOf(Registered);
242
+ expect(recovered.getName()).toBe('registered');
243
+
244
+ // Unregistered becomes plain object, loses methods
245
+ expect(recovered.dependency).toBeDefined();
246
+ expect(recovered.dependency.value).toBe(42);
247
+ expect(recovered.dependency).not.toBeInstanceOf(Unregistered);
248
+ expect(typeof recovered.dependency.getValue).toBe('undefined');
249
+ });
250
+
251
+ it('should handle array of unregistered classes', () => {
252
+ class Unregistered {
253
+ constructor(val = 0) { this.val = val; }
254
+ }
255
+
256
+ const arr = [
257
+ new Unregistered(1),
258
+ new Unregistered(2),
259
+ new Unregistered(3)
260
+ ];
261
+
262
+ superLs.set('unregArray', arr);
263
+ const recovered = superLs.get('unregArray');
264
+
265
+ expect(recovered[0].val).toBe(1);
266
+ expect(recovered[1].val).toBe(2);
267
+ expect(recovered[2].val).toBe(3);
268
+ // But they're plain objects now
269
+ expect(recovered[0]).not.toBeInstanceOf(Unregistered);
270
+ });
271
+ });
272
+
273
+ // ==========================================
274
+ // SPECIAL DATA TYPES
275
+ // ==========================================
276
+ describe('Special Data Types', () => {
277
+ it('should handle BigInt', () => {
278
+ const data = {
279
+ bigNumber: BigInt('9007199254740991000'),
280
+ normalNumber: 42
281
+ };
282
+
283
+ superLs.set('bigint', data);
284
+ const recovered = superLs.get('bigint');
285
+
286
+ expect(recovered.bigNumber).toBe(BigInt('9007199254740991000'));
287
+ expect(typeof recovered.bigNumber).toBe('bigint');
288
+ });
289
+
290
+ it('should handle RegExp', () => {
291
+ const data = {
292
+ pattern: /hello\s+world/gi,
293
+ email: /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/
294
+ };
295
+
296
+ superLs.set('regex', data);
297
+ const recovered = superLs.get('regex');
298
+
299
+ expect(recovered.pattern).toBeInstanceOf(RegExp);
300
+ expect(recovered.pattern.source).toBe('hello\\s+world');
301
+ expect(recovered.pattern.flags).toBe('gi');
302
+ expect(recovered.email.test('test@example.com')).toBe(true);
303
+ });
304
+
305
+ it('should handle Typed Arrays', () => {
306
+ const data = {
307
+ uint8: new Uint8Array([1, 2, 3, 4]),
308
+ float32: new Float32Array([1.5, 2.5, 3.5]),
309
+ int32: new Int32Array([-1, 0, 1])
310
+ };
311
+
312
+ superLs.set('typed', data);
313
+ const recovered = superLs.get('typed');
314
+
315
+ expect(recovered.uint8).toBeInstanceOf(Uint8Array);
316
+ expect(Array.from(recovered.uint8)).toEqual([1, 2, 3, 4]);
317
+ expect(recovered.float32).toBeInstanceOf(Float32Array);
318
+ expect(recovered.int32).toBeInstanceOf(Int32Array);
319
+ });
320
+
321
+ it('should handle sparse arrays (holes become undefined)', () => {
322
+ const sparse = [1, , , 4, , 6];
323
+ sparse[10] = 'ten';
324
+
325
+ superLs.set('sparse', sparse);
326
+ const recovered = superLs.get('sparse');
327
+
328
+ expect(recovered[0]).toBe(1);
329
+ expect(recovered[3]).toBe(4);
330
+ expect(recovered[5]).toBe(6);
331
+ expect(recovered[10]).toBe('ten');
332
+ // Note: devalue converts holes to undefined, doesn't preserve sparse arrays
333
+ // This is a known limitation
334
+ expect(recovered[1]).toBeUndefined();
335
+ });
336
+
337
+ it('should handle Object.create(null)', () => {
338
+ const nullProto = Object.create(null);
339
+ nullProto.name = 'no prototype';
340
+ nullProto.value = 123;
341
+
342
+ superLs.set('nullproto', nullProto);
343
+ const recovered = superLs.get('nullproto');
344
+
345
+ expect(recovered.name).toBe('no prototype');
346
+ expect(recovered.value).toBe(123);
347
+ });
348
+ });
349
+
350
+ // ==========================================
351
+ // DEEPLY NESTED STRUCTURES
352
+ // ==========================================
353
+ describe('Deeply Nested Structures', () => {
354
+ it('should handle 10 levels of nesting', () => {
355
+ class Node {
356
+ constructor(value = 0) {
357
+ this.value = value;
358
+ this.child = null;
359
+ }
360
+ getValue() { return this.value; }
361
+ }
362
+
363
+ superLs.register(Node);
364
+
365
+ // Create 10-level deep structure
366
+ let root = new Node(0);
367
+ let current = root;
368
+ for (let i = 1; i <= 10; i++) {
369
+ current.child = new Node(i);
370
+ current = current.child;
371
+ }
372
+
373
+ superLs.set('deep', root);
374
+ const recovered = superLs.get('deep');
375
+
376
+ // Verify all 10 levels
377
+ let node = recovered;
378
+ for (let i = 0; i <= 10; i++) {
379
+ expect(node).toBeInstanceOf(Node);
380
+ expect(node.getValue()).toBe(i);
381
+ node = node.child;
382
+ }
383
+ expect(node).toBeNull(); // After level 10
384
+ });
385
+
386
+ it('should handle mixed nested types', () => {
387
+ class Item {
388
+ constructor(name = '') { this.name = name; }
389
+ }
390
+
391
+ superLs.register(Item);
392
+
393
+ const complex = {
394
+ level1: {
395
+ array: [
396
+ new Map([['key', new Item('in map')]]),
397
+ new Set([new Item('in set')]),
398
+ {
399
+ nested: {
400
+ item: new Item('deeply nested')
401
+ }
402
+ }
403
+ ]
404
+ }
405
+ };
406
+
407
+ superLs.set('complex', complex);
408
+ const recovered = superLs.get('complex');
409
+
410
+ expect(recovered.level1.array[0].get('key')).toBeInstanceOf(Item);
411
+ expect(recovered.level1.array[2].nested.item).toBeInstanceOf(Item);
412
+ expect(recovered.level1.array[2].nested.item.name).toBe('deeply nested');
413
+ });
414
+ });
415
+
416
+ // ==========================================
417
+ // CONSTRUCTOR EDGE CASES
418
+ // ==========================================
419
+ describe('Constructor Edge Cases', () => {
420
+ it('should handle class with required constructor args using hydrate', () => {
421
+ class RequiredArgs {
422
+ constructor(a, b, c) {
423
+ if (a === undefined || b === undefined || c === undefined) {
424
+ throw new Error('All arguments required!');
425
+ }
426
+ this.a = a;
427
+ this.b = b;
428
+ this.c = c;
429
+ }
430
+
431
+ static hydrate(data) {
432
+ return new RequiredArgs(data.a, data.b, data.c);
433
+ }
434
+
435
+ sum() { return this.a + this.b + this.c; }
436
+ }
437
+
438
+ superLs.register(RequiredArgs);
439
+
440
+ const instance = new RequiredArgs(1, 2, 3);
441
+ superLs.set('required', instance);
442
+ const recovered = superLs.get('required');
443
+
444
+ expect(recovered).toBeInstanceOf(RequiredArgs);
445
+ expect(recovered.sum()).toBe(6);
446
+ });
447
+
448
+ it('should handle class with default constructor when no hydrate', () => {
449
+ class NoDefaultArgs {
450
+ constructor(value = 'default') {
451
+ this.value = value;
452
+ }
453
+ }
454
+
455
+ superLs.register(NoDefaultArgs);
456
+
457
+ const instance = new NoDefaultArgs('custom');
458
+ superLs.set('nodefault', instance);
459
+ const recovered = superLs.get('nodefault');
460
+
461
+ expect(recovered).toBeInstanceOf(NoDefaultArgs);
462
+ expect(recovered.value).toBe('custom');
463
+ });
464
+ });
465
+
466
+ // ==========================================
467
+ // GETTERS AND SETTERS
468
+ // ==========================================
469
+ describe('Getters and Setters', () => {
470
+ it('should NOT serialize computed getters (expected behavior)', () => {
471
+ class Rectangle {
472
+ constructor(width = 0, height = 0) {
473
+ this._width = width;
474
+ this._height = height;
475
+ }
476
+
477
+ get area() {
478
+ return this._width * this._height;
479
+ }
480
+
481
+ get perimeter() {
482
+ return 2 * (this._width + this._height);
483
+ }
484
+ }
485
+
486
+ superLs.register(Rectangle);
487
+
488
+ const rect = new Rectangle(10, 5);
489
+ expect(rect.area).toBe(50); // Works before serialization
490
+
491
+ superLs.set('rect', rect);
492
+ const recovered = superLs.get('rect');
493
+
494
+ // Getters work because they're on the prototype
495
+ expect(recovered._width).toBe(10);
496
+ expect(recovered._height).toBe(5);
497
+ expect(recovered.area).toBe(50);
498
+ expect(recovered.perimeter).toBe(30);
499
+ });
500
+ });
501
+
502
+ // ==========================================
503
+ // CLASS NAME COLLISIONS
504
+ // ==========================================
505
+ describe('Class Name Collisions', () => {
506
+ it('should handle classes with same name using custom typeName', () => {
507
+ // Simulating two modules with same class name
508
+ const ModuleA = (() => {
509
+ class User {
510
+ constructor() { this.type = 'A'; }
511
+ getType() { return `User from A: ${this.type}`; }
512
+ }
513
+ return User;
514
+ })();
515
+
516
+ const ModuleB = (() => {
517
+ class User {
518
+ constructor() { this.type = 'B'; }
519
+ getType() { return `User from B: ${this.type}`; }
520
+ }
521
+ return User;
522
+ })();
523
+
524
+ superLs.register(ModuleA, 'UserA');
525
+ superLs.register(ModuleB, 'UserB');
526
+
527
+ const userA = new ModuleA();
528
+ const userB = new ModuleB();
529
+
530
+ superLs.set('userA', userA);
531
+ superLs.set('userB', userB);
532
+
533
+ const recoveredA = superLs.get('userA');
534
+ const recoveredB = superLs.get('userB');
535
+
536
+ expect(recoveredA).toBeInstanceOf(ModuleA);
537
+ expect(recoveredB).toBeInstanceOf(ModuleB);
538
+ expect(recoveredA.getType()).toBe('User from A: A');
539
+ expect(recoveredB.getType()).toBe('User from B: B');
540
+ });
541
+ });
542
+
543
+ // ==========================================
544
+ // CIRCULAR WITH CLASSES
545
+ // ==========================================
546
+ describe('Circular References with Classes', () => {
547
+ it('should handle self-referencing class', () => {
548
+ class LinkedNode {
549
+ constructor(value = 0) {
550
+ this.value = value;
551
+ this.next = null;
552
+ this.prev = null;
553
+ }
554
+ }
555
+
556
+ superLs.register(LinkedNode);
557
+
558
+ // Create circular linked list
559
+ const node1 = new LinkedNode(1);
560
+ const node2 = new LinkedNode(2);
561
+ const node3 = new LinkedNode(3);
562
+
563
+ node1.next = node2;
564
+ node2.prev = node1;
565
+ node2.next = node3;
566
+ node3.prev = node2;
567
+ node3.next = node1; // Circular!
568
+ node1.prev = node3;
569
+
570
+ superLs.set('circular', node1);
571
+ const recovered = superLs.get('circular');
572
+
573
+ expect(recovered.value).toBe(1);
574
+ expect(recovered.next.value).toBe(2);
575
+ expect(recovered.next.next.value).toBe(3);
576
+ expect(recovered.next.next.next).toBe(recovered); // Circular preserved
577
+ expect(recovered.prev).toBe(recovered.next.next); // node3
578
+ });
579
+
580
+ it('should handle mutual circular references between different classes', () => {
581
+ class Author {
582
+ constructor(name = '') {
583
+ this.name = name;
584
+ this.books = [];
585
+ }
586
+ }
587
+
588
+ class Book {
589
+ constructor(title = '') {
590
+ this.title = title;
591
+ this.author = null;
592
+ }
593
+ }
594
+
595
+ superLs.register(Author);
596
+ superLs.register(Book);
597
+
598
+ const author = new Author('Stephen King');
599
+ const book1 = new Book('The Shining');
600
+ const book2 = new Book('IT');
601
+
602
+ book1.author = author;
603
+ book2.author = author;
604
+ author.books.push(book1, book2);
605
+
606
+ superLs.set('author', author);
607
+ const recovered = superLs.get('author');
608
+
609
+ expect(recovered).toBeInstanceOf(Author);
610
+ expect(recovered.books[0]).toBeInstanceOf(Book);
611
+ expect(recovered.books[0].author).toBe(recovered);
612
+ expect(recovered.books[1].author).toBe(recovered);
613
+ });
614
+ });
615
+
616
+ // ==========================================
617
+ // EXPLICIT UNDEFINED VALUES
618
+ // ==========================================
619
+ describe('Explicit Undefined Values', () => {
620
+ it('should preserve explicit undefined in objects', () => {
621
+ class Container {
622
+ constructor() {
623
+ this.defined = 'value';
624
+ this.explicitUndefined = undefined;
625
+ this.nullValue = null;
626
+ }
627
+ }
628
+
629
+ superLs.register(Container);
630
+
631
+ const container = new Container();
632
+ superLs.set('container', container);
633
+ const recovered = superLs.get('container');
634
+
635
+ expect(recovered.defined).toBe('value');
636
+ expect(recovered.explicitUndefined).toBeUndefined();
637
+ expect('explicitUndefined' in recovered).toBe(true);
638
+ expect(recovered.nullValue).toBeNull();
639
+ });
640
+
641
+ it('should handle undefined in arrays', () => {
642
+ const arr = [1, undefined, 3, undefined, 5];
643
+
644
+ superLs.set('undefArray', arr);
645
+ const recovered = superLs.get('undefArray');
646
+
647
+ expect(recovered[0]).toBe(1);
648
+ expect(recovered[1]).toBeUndefined();
649
+ expect(recovered[2]).toBe(3);
650
+ expect(recovered[3]).toBeUndefined();
651
+ expect(recovered[4]).toBe(5);
652
+ });
653
+ });
654
+
655
+ // ==========================================
656
+ // EDGE CASE: EMPTY STRUCTURES
657
+ // ==========================================
658
+ describe('Empty Structures', () => {
659
+ it('should handle empty class instance', () => {
660
+ class Empty {}
661
+
662
+ superLs.register(Empty);
663
+
664
+ const empty = new Empty();
665
+ superLs.set('empty', empty);
666
+ const recovered = superLs.get('empty');
667
+
668
+ expect(recovered).toBeInstanceOf(Empty);
669
+ });
670
+
671
+ it('should handle empty nested structures', () => {
672
+ const data = {
673
+ emptyObj: {},
674
+ emptyArr: [],
675
+ emptyMap: new Map(),
676
+ emptySet: new Set()
677
+ };
678
+
679
+ superLs.set('empties', data);
680
+ const recovered = superLs.get('empties');
681
+
682
+ expect(recovered.emptyObj).toEqual({});
683
+ expect(recovered.emptyArr).toEqual([]);
684
+ expect(recovered.emptyMap.size).toBe(0);
685
+ expect(recovered.emptySet.size).toBe(0);
686
+ });
687
+ });
688
+
689
+ // ==========================================
690
+ // SPECIAL KEYS
691
+ // ==========================================
692
+ describe('Special Property Names', () => {
693
+ it('should handle properties named like internal markers', () => {
694
+ const tricky = {
695
+ __super_type__: 'not a real type',
696
+ __data__: 'just data',
697
+ __proto__: { fake: true },
698
+ constructor: 'also fake',
699
+ normal: 'value'
700
+ };
701
+
702
+ superLs.set('tricky', tricky);
703
+ const recovered = superLs.get('tricky');
704
+
705
+ // This might be tricky - let's see behavior
706
+ expect(recovered.normal).toBe('value');
707
+ });
708
+
709
+ it('should handle numeric string keys', () => {
710
+ const obj = {
711
+ '0': 'zero',
712
+ '1': 'one',
713
+ '999': 'nine nine nine'
714
+ };
715
+
716
+ superLs.set('numeric', obj);
717
+ const recovered = superLs.get('numeric');
718
+
719
+ expect(recovered['0']).toBe('zero');
720
+ expect(recovered['1']).toBe('one');
721
+ expect(recovered['999']).toBe('nine nine nine');
722
+ });
723
+ });
724
+
725
+ // ==========================================
726
+ // STRESS TEST
727
+ // ==========================================
728
+ describe('Stress Tests', () => {
729
+ it('should handle large number of registered classes', () => {
730
+ // Register 50 classes
731
+ const classes = [];
732
+ for (let i = 0; i < 50; i++) {
733
+ const cls = class {
734
+ constructor() { this.id = i; }
735
+ getId() { return this.id; }
736
+ };
737
+ Object.defineProperty(cls, 'name', { value: `Class${i}` });
738
+ classes.push(cls);
739
+ superLs.register(cls);
740
+ }
741
+
742
+ // Create instances of each
743
+ const instances = classes.map((Cls, i) => {
744
+ const inst = new Cls();
745
+ inst.index = i;
746
+ return inst;
747
+ });
748
+
749
+ superLs.set('manyClasses', instances);
750
+ const recovered = superLs.get('manyClasses');
751
+
752
+ expect(recovered).toHaveLength(50);
753
+ for (let i = 0; i < 50; i++) {
754
+ expect(recovered[i]).toBeInstanceOf(classes[i]);
755
+ expect(recovered[i].getId()).toBe(i);
756
+ }
757
+ });
758
+
759
+ it('should handle large array of class instances', () => {
760
+ class SimpleItem {
761
+ constructor(id = 0) { this.id = id; }
762
+ }
763
+
764
+ superLs.register(SimpleItem);
765
+
766
+ const items = Array.from({ length: 1000 }, (_, i) => new SimpleItem(i));
767
+
768
+ superLs.set('largeArray', items);
769
+ const recovered = superLs.get('largeArray');
770
+
771
+ expect(recovered).toHaveLength(1000);
772
+ expect(recovered[0]).toBeInstanceOf(SimpleItem);
773
+ expect(recovered[500].id).toBe(500);
774
+ expect(recovered[999].id).toBe(999);
775
+ });
776
+ });
777
+
778
+ // ==========================================
779
+ // ERROR SCENARIOS
780
+ // ==========================================
781
+ describe('Error Scenarios', () => {
782
+ it('should return null for non-existent key', () => {
783
+ expect(superLs.get('does-not-exist')).toBeNull();
784
+ });
785
+
786
+ it('should throw when registering non-function', () => {
787
+ expect(() => superLs.register({})).toThrow();
788
+ expect(() => superLs.register('string')).toThrow();
789
+ expect(() => superLs.register(123)).toThrow();
790
+ expect(() => superLs.register(null)).toThrow();
791
+ });
792
+
793
+ it('should throw for non-serializable values (functions)', () => {
794
+ const data = {
795
+ name: 'test',
796
+ fn: () => 'hello'
797
+ };
798
+ expect(() => superLs.set('withFn', data)).toThrow();
799
+ });
800
+
801
+ it('should silently ignore WeakMap (becomes empty object)', () => {
802
+ // WeakMap cannot be serialized - devalue silently converts to empty object
803
+ // This is a limitation, not an error
804
+ const data = { wm: new WeakMap(), other: 'value' };
805
+ superLs.set('withWm', data);
806
+ const recovered = superLs.get('withWm');
807
+ expect(recovered.other).toBe('value');
808
+ // WeakMap becomes empty object
809
+ expect(recovered.wm).toEqual({});
810
+ });
811
+
812
+ it('should silently ignore WeakSet (becomes empty object)', () => {
813
+ // WeakSet cannot be serialized - devalue silently converts to empty object
814
+ const data = { ws: new WeakSet(), other: 'value' };
815
+ superLs.set('withWs', data);
816
+ const recovered = superLs.get('withWs');
817
+ expect(recovered.other).toBe('value');
818
+ // WeakSet becomes empty object
819
+ expect(recovered.ws).toEqual({});
820
+ });
821
+ });
822
+
823
+ // ==========================================
824
+ // MULTIPLE INSTANCES
825
+ // ==========================================
826
+ describe('Multiple SuperLocalStorage Instances', () => {
827
+ it('should have isolated registries', () => {
828
+ class OnlyInA {
829
+ constructor() { this.source = 'A'; }
830
+ getSource() { return this.source; }
831
+ }
832
+
833
+ const instanceA = new SuperLocalStorage();
834
+ const instanceB = new SuperLocalStorage();
835
+
836
+ instanceA.register(OnlyInA);
837
+ // Note: NOT registered in instanceB
838
+
839
+ instanceA.set('test', new OnlyInA());
840
+
841
+ const fromA = instanceA.get('test');
842
+ const fromB = instanceB.get('test');
843
+
844
+ expect(fromA).toBeInstanceOf(OnlyInA);
845
+ expect(fromA.getSource()).toBe('A');
846
+
847
+ // instanceB can read the data but won't rehydrate as class
848
+ expect(fromB).not.toBeInstanceOf(OnlyInA);
849
+ });
850
+ });
851
+
852
+ // ==========================================
853
+ // SET CONTAINING CLASS INSTANCES
854
+ // ==========================================
855
+ describe('Set with Class Instances', () => {
856
+ it('should handle Set containing class instances', () => {
857
+ class Tag {
858
+ constructor(name = '') { this.name = name; }
859
+ getName() { return this.name; }
860
+ }
861
+
862
+ superLs.register(Tag);
863
+
864
+ const tags = new Set([
865
+ new Tag('javascript'),
866
+ new Tag('typescript'),
867
+ new Tag('nodejs')
868
+ ]);
869
+
870
+ superLs.set('tags', tags);
871
+ const recovered = superLs.get('tags');
872
+
873
+ expect(recovered).toBeInstanceOf(Set);
874
+ expect(recovered.size).toBe(3);
875
+
876
+ const arr = Array.from(recovered);
877
+ expect(arr[0]).toBeInstanceOf(Tag);
878
+ expect(arr[0].getName()).toBe('javascript');
879
+ });
880
+ });
881
+
882
+ // ==========================================
883
+ // MAP WITH CLASS KEYS
884
+ // ==========================================
885
+ describe('Map with Complex Keys', () => {
886
+ it('should handle Map with class instances as keys', () => {
887
+ class Key {
888
+ constructor(id = '') { this.id = id; }
889
+ }
890
+
891
+ superLs.register(Key);
892
+
893
+ const key1 = new Key('k1');
894
+ const key2 = new Key('k2');
895
+
896
+ const map = new Map();
897
+ map.set(key1, 'value1');
898
+ map.set(key2, 'value2');
899
+
900
+ superLs.set('mapWithClassKeys', map);
901
+ const recovered = superLs.get('mapWithClassKeys');
902
+
903
+ expect(recovered).toBeInstanceOf(Map);
904
+ expect(recovered.size).toBe(2);
905
+
906
+ const keys = Array.from(recovered.keys());
907
+ expect(keys[0]).toBeInstanceOf(Key);
908
+ expect(keys[0].id).toBe('k1');
909
+ });
910
+ });
911
+ });