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.
- package/EXPLAIN.md +612 -0
- package/README.md +486 -0
- package/TEST_DOCUMENTATION.md +255 -0
- package/index.js +615 -0
- package/jsconfig.json +13 -0
- package/mkctx.config.json +7 -0
- package/package.json +31 -0
- package/tests/super-ls.edge-cases.spec.js +911 -0
- package/tests/super-ls.normal-cases.spec.js +794 -0
|
@@ -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
|
+
});
|