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 ADDED
@@ -0,0 +1,612 @@
1
+ # SuperLocalStorage - Technical Deep Dive
2
+
3
+ > A comprehensive guide to understanding the internal architecture and implementation details of SuperLocalStorage.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Architecture Overview](#architecture-overview)
8
+ 2. [Core Concepts](#core-concepts)
9
+ 3. [Serialization Pipeline](#serialization-pipeline)
10
+ 4. [Deserialization Pipeline](#deserialization-pipeline)
11
+ 5. [Circular Reference Handling](#circular-reference-handling)
12
+ 6. [Class Registration System](#class-registration-system)
13
+ 7. [Data Flow Diagrams](#data-flow-diagrams)
14
+ 8. [Design Decisions](#design-decisions)
15
+
16
+ ---
17
+
18
+ ## Architecture Overview
19
+
20
+ SuperLocalStorage acts as a middleware layer between your application and Titan Planet's `t.ls` storage API. It leverages the [devalue](https://github.com/Rich-Harris/devalue) library for serialization while adding custom class hydration support.
21
+
22
+ ```
23
+ ┌─────────────────────────────────────────────────────────────────┐
24
+ │ Your Application │
25
+ └─────────────────────────────────────────────────────────────────┘
26
+
27
+
28
+ ┌─────────────────────────────────────────────────────────────────┐
29
+ │ SuperLocalStorage │
30
+ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
31
+ │ │ Class Registry │ │ Serialization │ │ Rehydration │ │
32
+ │ │ (Map<name, │ │ Pipeline │ │ Pipeline │ │
33
+ │ │ Constructor>)│ │ │ │ │ │
34
+ │ └─────────────────┘ └─────────────────┘ └─────────────┘ │
35
+ └─────────────────────────────────────────────────────────────────┘
36
+
37
+
38
+ ┌─────────────────────────────────────────────────────────────────┐
39
+ │ devalue (stringify/parse) │
40
+ │ Handles: Map, Set, Date, RegExp, BigInt, etc. │
41
+ └─────────────────────────────────────────────────────────────────┘
42
+
43
+
44
+ ┌─────────────────────────────────────────────────────────────────┐
45
+ │ t.ls (Titan Planet API) │
46
+ │ Native string key-value store │
47
+ └─────────────────────────────────────────────────────────────────┘
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Core Concepts
53
+
54
+ ### 1. Type Markers
55
+
56
+ Custom class instances are wrapped with metadata markers before serialization:
57
+
58
+ ```javascript
59
+ // Constants used for metadata
60
+ const TYPE_MARKER = '__super_type__'; // Stores the registered class name
61
+ const DATA_MARKER = '__data__'; // Stores the serialized properties
62
+ ```
63
+
64
+ ### 2. The Wrapper Structure
65
+
66
+ When a registered class instance is serialized, it becomes:
67
+
68
+ ```javascript
69
+ // Original instance
70
+ const player = new Player('Alice', 100);
71
+
72
+ // After _toSerializable() transformation
73
+ {
74
+ __super_type__: 'Player', // Class identifier
75
+ __data__: { // All own properties
76
+ name: 'Alice',
77
+ score: 100
78
+ }
79
+ }
80
+ ```
81
+
82
+ ### 3. The Registry
83
+
84
+ A `Map` that associates type names with their constructors:
85
+
86
+ ```javascript
87
+ this.registry = new Map();
88
+ // After registration:
89
+ // 'Player' → class Player { ... }
90
+ // 'Weapon' → class Weapon { ... }
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Serialization Pipeline
96
+
97
+ The `set(key, value)` method triggers the serialization pipeline:
98
+
99
+ ```
100
+ set(key, value)
101
+
102
+
103
+ _toSerializable(value, seen)
104
+
105
+ ├─── isPrimitive? ──────────────────► return value
106
+
107
+ ├─── seen.has(value)? ──────────────► return seen.get(value) [circular ref]
108
+
109
+ ├─── _tryWrapRegisteredClass() ─────► { __super_type__, __data__ }
110
+ │ │
111
+ │ └─── recursively process all properties
112
+
113
+ ├─── _isNativelySerializable()? ────► return value [Date, RegExp, TypedArray]
114
+
115
+ └─── _serializeCollection()
116
+
117
+ ├─── Array → _serializeArray()
118
+ ├─── Map → _serializeMap()
119
+ ├─── Set → _serializeSet()
120
+ └─── Object → _serializeObject()
121
+
122
+ └─── recursively process all values
123
+
124
+
125
+ stringify(payload) ← devalue library
126
+
127
+
128
+ t.ls.set(prefixedKey, serializedString)
129
+ ```
130
+
131
+ ### Key Methods Explained
132
+
133
+ #### `_toSerializable(value, seen)`
134
+
135
+ The main recursive transformation function. It:
136
+
137
+ 1. **Short-circuits primitives** - Returns immediately for `null`, `undefined`, numbers, strings, booleans
138
+ 2. **Handles circular references** - Uses `WeakMap` to track already-processed objects
139
+ 3. **Prioritizes registered classes** - Checks class registry BEFORE native types
140
+ 4. **Delegates to specialists** - Routes to type-specific serializers
141
+
142
+ ```javascript
143
+ _toSerializable(value, seen = new WeakMap()) {
144
+ if (isPrimitive(value)) return value;
145
+ if (seen.has(value)) return seen.get(value); // Circular reference!
146
+
147
+ const classWrapper = this._tryWrapRegisteredClass(value, seen);
148
+ if (classWrapper) return classWrapper;
149
+
150
+ if (this._isNativelySerializable(value)) return value;
151
+
152
+ return this._serializeCollection(value, seen);
153
+ }
154
+ ```
155
+
156
+ #### `_tryWrapRegisteredClass(value, seen)`
157
+
158
+ Checks if the value is an instance of any registered class:
159
+
160
+ ```javascript
161
+ _tryWrapRegisteredClass(value, seen) {
162
+ for (const [name, Constructor] of this.registry.entries()) {
163
+ if (value instanceof Constructor) {
164
+ // Create wrapper FIRST (for circular ref tracking)
165
+ const wrapper = {
166
+ [TYPE_MARKER]: name,
167
+ [DATA_MARKER]: {}
168
+ };
169
+
170
+ // Register in seen map BEFORE recursing (prevents infinite loops)
171
+ seen.set(value, wrapper);
172
+
173
+ // Now safely recurse into properties
174
+ for (const key of Object.keys(value)) {
175
+ wrapper[DATA_MARKER][key] = this._toSerializable(value[key], seen);
176
+ }
177
+
178
+ return wrapper;
179
+ }
180
+ }
181
+ return null; // Not a registered class
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Deserialization Pipeline
188
+
189
+ The `get(key)` method triggers the deserialization pipeline:
190
+
191
+ ```
192
+ get(key)
193
+
194
+
195
+ t.ls.get(prefixedKey)
196
+
197
+ ├─── null? ────────────────────────────► return null
198
+
199
+
200
+ parse(raw) ← devalue library (restores Map, Set, Date, etc.)
201
+
202
+
203
+ _rehydrate(parsed, seen)
204
+
205
+ ├─── isPrimitive? ─────────────────────► return value
206
+
207
+ ├─── seen.has(value)? ─────────────────► return seen.get(value) [circular ref]
208
+
209
+ ├─── hasTypeWrapper()? ────────────────► _rehydrateClass()
210
+ │ │
211
+ │ ├─── Create placeholder object
212
+ │ ├─── Register in seen map
213
+ │ ├─── Recursively rehydrate __data__ properties
214
+ │ ├─── Create instance via hydrate() or new Constructor()
215
+ │ └─── Morph placeholder into instance
216
+
217
+ ├─── Date or RegExp? ──────────────────► return value
218
+
219
+ └─── _rehydrateCollection()
220
+
221
+ ├─── Array → _rehydrateArray()
222
+ ├─── Map → _rehydrateMap()
223
+ ├─── Set → _rehydrateSet()
224
+ └─── Object → _rehydrateObject()
225
+ ```
226
+
227
+ ### Key Methods Explained
228
+
229
+ #### `_rehydrateClass(value, seen)`
230
+
231
+ The most complex method - restores class instances:
232
+
233
+ ```javascript
234
+ _rehydrateClass(value, seen) {
235
+ const Constructor = this.registry.get(value[TYPE_MARKER]);
236
+
237
+ if (!Constructor) {
238
+ // Class not registered - treat as plain object
239
+ return this._rehydrateObject(value, seen);
240
+ }
241
+
242
+ // CRITICAL: Create placeholder BEFORE recursing
243
+ // This placeholder will be used for circular references
244
+ const placeholder = {};
245
+ seen.set(value, placeholder);
246
+
247
+ // Recursively rehydrate all nested data
248
+ const hydratedData = {};
249
+ for (const key of Object.keys(value[DATA_MARKER])) {
250
+ hydratedData[key] = this._rehydrate(value[DATA_MARKER][key], seen);
251
+ }
252
+
253
+ // Create the actual instance
254
+ const instance = this._createInstance(Constructor, hydratedData);
255
+
256
+ // MAGIC: Transform placeholder into the actual instance
257
+ // Any circular references pointing to placeholder now point to instance
258
+ Object.assign(placeholder, instance);
259
+ Object.setPrototypeOf(placeholder, Object.getPrototypeOf(instance));
260
+
261
+ return placeholder;
262
+ }
263
+ ```
264
+
265
+ #### `_createInstance(Constructor, data)`
266
+
267
+ Handles both simple and complex constructors:
268
+
269
+ ```javascript
270
+ _createInstance(Constructor, data) {
271
+ // If class has static hydrate(), use it (for complex constructors)
272
+ if (typeof Constructor.hydrate === 'function') {
273
+ return Constructor.hydrate(data);
274
+ }
275
+
276
+ // Otherwise, create empty instance and assign properties
277
+ const instance = new Constructor();
278
+ Object.assign(instance, data);
279
+ return instance;
280
+ }
281
+ ```
282
+
283
+ ---
284
+
285
+ ## Circular Reference Handling
286
+
287
+ Circular references are handled through a `WeakMap` called `seen` that tracks processed objects.
288
+
289
+ ### The Problem
290
+
291
+ ```javascript
292
+ const parent = new Parent('John');
293
+ const child = new Child('Jane');
294
+ parent.child = child;
295
+ child.parent = parent; // Circular!
296
+ ```
297
+
298
+ Without protection, recursion would be infinite:
299
+ ```
300
+ serialize(parent)
301
+ → serialize(parent.child)
302
+ → serialize(child.parent)
303
+ → serialize(parent.child) // Infinite loop!
304
+ ```
305
+
306
+ ### The Solution: Pre-registration
307
+
308
+ ```javascript
309
+ _tryWrapRegisteredClass(value, seen) {
310
+ // 1. Create wrapper structure
311
+ const wrapper = { __super_type__: name, __data__: {} };
312
+
313
+ // 2. Register BEFORE recursing into properties
314
+ seen.set(value, wrapper);
315
+
316
+ // 3. Now safe to recurse - if we encounter this object again,
317
+ // seen.has(value) returns true and we return the existing wrapper
318
+ for (const key of Object.keys(value)) {
319
+ wrapper[DATA_MARKER][key] = this._toSerializable(value[key], seen);
320
+ }
321
+ }
322
+ ```
323
+
324
+ ### Rehydration with Placeholders
325
+
326
+ During deserialization, we use a "placeholder morphing" technique:
327
+
328
+ ```javascript
329
+ // 1. Create empty placeholder
330
+ const placeholder = {};
331
+ seen.set(value, placeholder);
332
+
333
+ // 2. Recurse - any circular refs get the placeholder
334
+ const hydratedData = { /* ... recursive calls ... */ };
335
+
336
+ // 3. Create real instance
337
+ const instance = new Constructor();
338
+ Object.assign(instance, hydratedData);
339
+
340
+ // 4. MORPH placeholder into instance
341
+ Object.assign(placeholder, instance); // Copy properties
342
+ Object.setPrototypeOf(placeholder, Constructor.prototype); // Set prototype
343
+
344
+ // Now placeholder IS the instance, and all circular refs work!
345
+ ```
346
+
347
+ ### Visual Example
348
+
349
+ ```
350
+ Before morphing:
351
+ ┌────────────────┐ ┌────────────────┐
352
+ │ parent │ │ child │
353
+ │ ┌────────────┐ │ │ ┌────────────┐ │
354
+ │ │ child: ────┼─┼─────┼─►placeholder │ │
355
+ │ └────────────┘ │ │ └────────────┘ │
356
+ │ ┌────────────┐ │ │ ┌────────────┐ │
357
+ │ │ name:'John'│ │ │ │ parent: ───┼─┼──► placeholder (for parent)
358
+ │ └────────────┘ │ │ └────────────┘ │
359
+ └────────────────┘ └────────────────┘
360
+
361
+ After morphing (placeholder becomes actual Child instance):
362
+ ┌────────────────┐ ┌────────────────┐
363
+ │ parent │ │ child (was │
364
+ │ (Child inst.) │ │ placeholder) │
365
+ │ ┌────────────┐ │ │ ┌────────────┐ │
366
+ │ │ child: ────┼─┼─────┼─► Child inst │ │
367
+ │ └────────────┘ │ │ └────────────┘ │
368
+ └────────────────┘ └────────────────┘
369
+ ```
370
+
371
+ ---
372
+
373
+ ## Class Registration System
374
+
375
+ ### Registration Flow
376
+
377
+ ```javascript
378
+ superLs.register(Player);
379
+ // Internally:
380
+ // 1. Validate: typeof Player === 'function' ✓
381
+ // 2. Get name: Player.name → 'Player'
382
+ // 3. Store: registry.set('Player', Player)
383
+
384
+ superLs.register(Player, 'GamePlayer');
385
+ // Same but uses custom name:
386
+ // registry.set('GamePlayer', Player)
387
+ ```
388
+
389
+ ### Why Custom Names?
390
+
391
+ 1. **Minification** - In production, `Player.name` might become `t` or `n`
392
+ 2. **Name Collisions** - Two modules might export `class User`
393
+ 3. **Versioning** - `UserV1`, `UserV2` for migration scenarios
394
+
395
+ ### The `hydrate()` Pattern
396
+
397
+ For classes with complex constructors:
398
+
399
+ ```javascript
400
+ class ImmutableUser {
401
+ constructor(name, email) {
402
+ if (!name || !email) throw new Error('Required!');
403
+ this.name = name;
404
+ this.email = email;
405
+ Object.freeze(this);
406
+ }
407
+
408
+ // Without hydrate(), deserialization would fail because
409
+ // new ImmutableUser() throws an error
410
+
411
+ static hydrate(data) {
412
+ return new ImmutableUser(data.name, data.email);
413
+ }
414
+ }
415
+ ```
416
+
417
+ ---
418
+
419
+ ## Data Flow Diagrams
420
+
421
+ ### Complete Serialization Flow
422
+
423
+ ```
424
+ ┌─────────────────────────────────────────────────────────────────────────┐
425
+ │ INPUT: { player: Player { name: 'Alice', weapon: Weapon { dmg: 50 } } }│
426
+ └─────────────────────────────────────────────────────────────────────────┘
427
+
428
+
429
+ ┌─────────────────────────────────────────────────────────────────────────┐
430
+ │ _toSerializable() - Process root object │
431
+ │ seen = WeakMap { } │
432
+ └─────────────────────────────────────────────────────────────────────────┘
433
+
434
+ ┌─────────────────────────┼─────────────────────────┐
435
+ ▼ ▼ ▼
436
+ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐
437
+ │ key: 'player' │ │ Player instance │ │ Weapon instance │
438
+ │ (string, skip) │ │ IS registered │ │ IS registered │
439
+ └──────────────────┘ │ Wrap it! │ │ Wrap it! │
440
+ └──────────────────┘ └──────────────────────┘
441
+ │ │
442
+ ▼ ▼
443
+ ┌─────────────────────────────────────────────────────────────────────────┐
444
+ │ OUTPUT (before devalue): │
445
+ │ { │
446
+ │ player: { │
447
+ │ __super_type__: 'Player', │
448
+ │ __data__: { │
449
+ │ name: 'Alice', │
450
+ │ weapon: { │
451
+ │ __super_type__: 'Weapon', │
452
+ │ __data__: { dmg: 50 } │
453
+ │ } │
454
+ │ } │
455
+ │ } │
456
+ │ } │
457
+ └─────────────────────────────────────────────────────────────────────────┘
458
+
459
+ ▼ devalue.stringify()
460
+ ┌─────────────────────────────────────────────────────────────────────────┐
461
+ │ STORED STRING: '[{"player":1},{"__super_type__":2,"__data__":3},...' │
462
+ └─────────────────────────────────────────────────────────────────────────┘
463
+ ```
464
+
465
+ ### Complete Deserialization Flow
466
+
467
+ ```
468
+ ┌─────────────────────────────────────────────────────────────────────────┐
469
+ │ STORED STRING: '[{"player":1},{"__super_type__":2,"__data__":3},...' │
470
+ └─────────────────────────────────────────────────────────────────────────┘
471
+
472
+ ▼ devalue.parse()
473
+ ┌─────────────────────────────────────────────────────────────────────────┐
474
+ │ PARSED (with __super_type__ markers intact): │
475
+ │ { player: { __super_type__: 'Player', __data__: { ... } } } │
476
+ └─────────────────────────────────────────────────────────────────────────┘
477
+
478
+
479
+ ┌─────────────────────────────────────────────────────────────────────────┐
480
+ │ _rehydrate() - Detect __super_type__ marker │
481
+ │ Look up 'Player' in registry → Found! │
482
+ └─────────────────────────────────────────────────────────────────────────┘
483
+
484
+
485
+ ┌─────────────────────────────────────────────────────────────────────────┐
486
+ │ _rehydrateClass(): │
487
+ │ 1. placeholder = {} │
488
+ │ 2. seen.set(original, placeholder) │
489
+ │ 3. hydratedData = { name: 'Alice', weapon: <recurse...> } │
490
+ │ 4. instance = new Player(); Object.assign(instance, hydratedData) │
491
+ │ 5. Object.assign(placeholder, instance) │
492
+ │ 6. Object.setPrototypeOf(placeholder, Player.prototype) │
493
+ └─────────────────────────────────────────────────────────────────────────┘
494
+
495
+
496
+ ┌─────────────────────────────────────────────────────────────────────────┐
497
+ │ OUTPUT: { player: Player { name: 'Alice', weapon: Weapon { dmg: 50 } } │
498
+ │ ✓ player instanceof Player │
499
+ │ ✓ player.weapon instanceof Weapon │
500
+ │ ✓ player.attack() works (methods restored via prototype) │
501
+ └─────────────────────────────────────────────────────────────────────────┘
502
+ ```
503
+
504
+ ---
505
+
506
+ ## Design Decisions
507
+
508
+ ### 1. Why Check Registered Classes First?
509
+
510
+ ```javascript
511
+ // In _toSerializable():
512
+ const classWrapper = this._tryWrapRegisteredClass(value, seen);
513
+ if (classWrapper) return classWrapper;
514
+
515
+ if (this._isNativelySerializable(value)) return value;
516
+ ```
517
+
518
+ **Reason**: A registered class might extend `Date` or another native type. By checking registered classes first, we ensure custom serialization takes precedence.
519
+
520
+ ### 2. Why Use `Object.setPrototypeOf()` Instead of Returning the Instance Directly?
521
+
522
+ ```javascript
523
+ // We do this:
524
+ Object.assign(placeholder, instance);
525
+ Object.setPrototypeOf(placeholder, Object.getPrototypeOf(instance));
526
+ return placeholder;
527
+
528
+ // Instead of:
529
+ return instance;
530
+ ```
531
+
532
+ **Reason**: Circular references already point to `placeholder`. If we return `instance`, those references become stale. By morphing `placeholder` into `instance`, all existing references remain valid.
533
+
534
+ ### 3. Why Separate `_serializeX` and `_rehydrateX` Methods?
535
+
536
+ **Reasons**:
537
+ - **Single Responsibility**: Each method handles one type
538
+ - **Testability**: Individual methods can be unit tested
539
+ - **Readability**: Clear what each method does
540
+ - **Extensibility**: Easy to add new type handlers
541
+
542
+ ### 4. Why Use `WeakMap` for `seen`?
543
+
544
+ ```javascript
545
+ _toSerializable(value, seen = new WeakMap())
546
+ ```
547
+
548
+ **Reasons**:
549
+ - **Memory efficiency**: WeakMap allows garbage collection of processed objects
550
+ - **No memory leaks**: References don't prevent cleanup
551
+ - **Object keys**: WeakMap allows objects as keys (regular Map would work but less efficiently)
552
+
553
+ ### 5. Why `Object.keys()` Instead of `for...in`?
554
+
555
+ ```javascript
556
+ for (const key of Object.keys(value)) {
557
+ // ...
558
+ }
559
+ ```
560
+
561
+ **Reason**: `Object.keys()` returns only own enumerable properties. `for...in` would include inherited properties, which we don't want to serialize.
562
+
563
+ ### 6. Why the Prefix System?
564
+
565
+ ```javascript
566
+ this.prefix = 'sls_';
567
+ t.ls.set(this.prefix + key, serialized);
568
+ ```
569
+
570
+ **Reasons**:
571
+ - **Namespace isolation**: Prevents collisions with other storage users
572
+ - **Easy identification**: All SuperLocalStorage keys are identifiable
573
+ - **Bulk operations**: Could implement `clearAll()` by prefix matching
574
+
575
+ ---
576
+
577
+ ## Performance Considerations
578
+
579
+ ### Time Complexity
580
+
581
+ | Operation | Complexity | Notes |
582
+ |-----------|------------|-------|
583
+ | `register()` | O(1) | Map insertion |
584
+ | `set()` | O(n) | n = total properties in object graph |
585
+ | `get()` | O(n) | n = total properties in object graph |
586
+ | Class lookup | O(k) | k = number of registered classes |
587
+
588
+ ### Memory Considerations
589
+
590
+ - **WeakMap for `seen`**: Allows GC of intermediate objects
591
+ - **Placeholder pattern**: Temporarily doubles memory for circular structures
592
+ - **String storage**: Final serialized form is a string (browser limitation)
593
+
594
+ ### Optimization Opportunities
595
+
596
+ 1. **Class lookup cache**: Could use `instanceof` checks once and cache results
597
+ 2. **Streaming serialization**: For very large objects
598
+ 3. **Compression**: For string-heavy data
599
+
600
+ ---
601
+
602
+ ## Summary
603
+
604
+ SuperLocalStorage provides a transparent serialization layer that:
605
+
606
+ 1. **Wraps** registered class instances with type metadata
607
+ 2. **Delegates** native type handling to devalue
608
+ 3. **Tracks** circular references via WeakMap
609
+ 4. **Morphs** placeholders for reference integrity
610
+ 5. **Restores** class prototypes for method access
611
+
612
+ The design prioritizes correctness over performance, with special attention to edge cases like circular references, inheritance, and complex constructor requirements.