@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/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
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.
|