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
package/README.md
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# ๐ช @titanpl/super-ls
|
|
2
|
+
|
|
3
|
+
> A supercharged storage adapter for Titan Planet that enables storing complex objects, circular references, and Class instances with automatic rehydration.
|
|
4
|
+
|
|
5
|
+
`super-ls` extends the capabilities of the native `t.ls` API by using `devalue` for serialization. While standard `t.ls` is limited to simple JSON data, `super-ls` allows you to save and retrieve rich data structures effortlessly.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## โจ Features
|
|
10
|
+
|
|
11
|
+
- **Rich Data Types**: Store `Map`, `Set`, `Date`, `RegExp`, `BigInt`, `TypedArray`, `undefined`, `NaN`, `Infinity`, and circular references
|
|
12
|
+
- **Class Hydration**: Register your custom classes and retrieve fully functional instances with methods intact
|
|
13
|
+
- **Dependency Injection Support**: Serialize/deserialize nested class instances and complex object graphs
|
|
14
|
+
- **Circular Reference Handling**: Automatic detection and preservation of circular references
|
|
15
|
+
- **Drop-in Library**: Works via standard ES module `import` without polluting the global `t` namespace
|
|
16
|
+
- **Titan Native Integration**: Built on top of `@titanpl/core`'s `t.ls` for persistence
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## ๐ฆ Installation
|
|
21
|
+
|
|
22
|
+
Add `super-ls` to your Titan Planet project:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @david200197/super-ls
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## ๐ Usage
|
|
31
|
+
|
|
32
|
+
### Basic Usage (Rich Data Types)
|
|
33
|
+
|
|
34
|
+
Store objects that standard JSON cannot handle:
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
import superLs from "@titanpl/super-ls";
|
|
38
|
+
|
|
39
|
+
// Maps
|
|
40
|
+
const settings = new Map([
|
|
41
|
+
["theme", "dark"],
|
|
42
|
+
["language", "en"]
|
|
43
|
+
]);
|
|
44
|
+
superLs.set("user_settings", settings);
|
|
45
|
+
|
|
46
|
+
const recovered = superLs.get("user_settings");
|
|
47
|
+
console.log(recovered instanceof Map); // true
|
|
48
|
+
console.log(recovered.get("theme")); // "dark"
|
|
49
|
+
|
|
50
|
+
// Sets
|
|
51
|
+
superLs.set("tags", new Set(["javascript", "typescript", "nodejs"]));
|
|
52
|
+
|
|
53
|
+
// Dates
|
|
54
|
+
superLs.set("lastLogin", new Date());
|
|
55
|
+
|
|
56
|
+
// RegExp
|
|
57
|
+
superLs.set("emailPattern", /^[\w-]+@[\w-]+\.\w+$/i);
|
|
58
|
+
|
|
59
|
+
// BigInt
|
|
60
|
+
superLs.set("bigNumber", BigInt("9007199254740991000"));
|
|
61
|
+
|
|
62
|
+
// Circular References
|
|
63
|
+
const obj = { name: "circular" };
|
|
64
|
+
obj.self = obj;
|
|
65
|
+
superLs.set("circular", obj);
|
|
66
|
+
|
|
67
|
+
const restored = superLs.get("circular");
|
|
68
|
+
console.log(restored.self === restored); // true
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Class Hydration
|
|
72
|
+
|
|
73
|
+
The true power of `super-ls` lies in its ability to restore class instances with their methods intact.
|
|
74
|
+
|
|
75
|
+
#### 1. Define and Register Your Class
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
import superLs from "@titanpl/super-ls";
|
|
79
|
+
|
|
80
|
+
class Player {
|
|
81
|
+
constructor(name = "", score = 0) {
|
|
82
|
+
this.name = name;
|
|
83
|
+
this.score = score;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
greet() {
|
|
87
|
+
return `Hello, I am ${this.name}!`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
addScore(points) {
|
|
91
|
+
this.score += points;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Register before saving or loading
|
|
96
|
+
superLs.register(Player);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### 2. Save and Restore
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
const player = new Player("Alice", 100);
|
|
103
|
+
superLs.set("player_1", player);
|
|
104
|
+
|
|
105
|
+
// Later, in a different request...
|
|
106
|
+
const restored = superLs.get("player_1");
|
|
107
|
+
|
|
108
|
+
console.log(restored.name); // "Alice"
|
|
109
|
+
console.log(restored.greet()); // "Hello, I am Alice!"
|
|
110
|
+
console.log(restored instanceof Player); // true
|
|
111
|
+
|
|
112
|
+
restored.addScore(50); // Methods work!
|
|
113
|
+
console.log(restored.score); // 150
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Dependency Injection Pattern
|
|
117
|
+
|
|
118
|
+
`super-ls` supports nested class instances, making it perfect for DI patterns:
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
class Weapon {
|
|
122
|
+
constructor(name = "", damage = 0) {
|
|
123
|
+
this.name = name;
|
|
124
|
+
this.damage = damage;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
attack() {
|
|
128
|
+
return `${this.name} deals ${this.damage} damage!`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class Warrior {
|
|
133
|
+
constructor(name = "", weapon = null) {
|
|
134
|
+
this.name = name;
|
|
135
|
+
this.weapon = weapon;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fight() {
|
|
139
|
+
if (!this.weapon) return `${this.name} has no weapon!`;
|
|
140
|
+
return `${this.name}: ${this.weapon.attack()}`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Register ALL classes in the dependency chain
|
|
145
|
+
superLs.register(Weapon);
|
|
146
|
+
superLs.register(Warrior);
|
|
147
|
+
|
|
148
|
+
// Create nested instances
|
|
149
|
+
const sword = new Weapon("Excalibur", 50);
|
|
150
|
+
const arthur = new Warrior("Arthur", sword);
|
|
151
|
+
|
|
152
|
+
superLs.set("hero", arthur);
|
|
153
|
+
|
|
154
|
+
// Restore with full dependency graph
|
|
155
|
+
const restored = superLs.get("hero");
|
|
156
|
+
console.log(restored instanceof Warrior); // true
|
|
157
|
+
console.log(restored.weapon instanceof Weapon); // true
|
|
158
|
+
console.log(restored.fight()); // "Arthur: Excalibur deals 50 damage!"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Custom Hydration (Complex Constructors)
|
|
162
|
+
|
|
163
|
+
For classes with required constructor arguments or complex initialization:
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
class ImmutableUser {
|
|
167
|
+
constructor(id, email) {
|
|
168
|
+
if (!id || !email) throw new Error("id and email required!");
|
|
169
|
+
this.id = id;
|
|
170
|
+
this.email = email;
|
|
171
|
+
Object.freeze(this);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Static hydrate method for custom reconstruction
|
|
175
|
+
static hydrate(data) {
|
|
176
|
+
return new ImmutableUser(data.id, data.email);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
superLs.register(ImmutableUser);
|
|
181
|
+
|
|
182
|
+
const user = new ImmutableUser(1, "alice@example.com");
|
|
183
|
+
superLs.set("user", user);
|
|
184
|
+
|
|
185
|
+
const restored = superLs.get("user"); // Works! Uses hydrate() internally
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Custom Type Names
|
|
189
|
+
|
|
190
|
+
Useful for minified code or avoiding name collisions:
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
// Two modules both export "User" class
|
|
194
|
+
import { User as AdminUser } from "./admin";
|
|
195
|
+
import { User as CustomerUser } from "./customer";
|
|
196
|
+
|
|
197
|
+
superLs.register(AdminUser, "AdminUser");
|
|
198
|
+
superLs.register(CustomerUser, "CustomerUser");
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Multiple Storage Instances
|
|
202
|
+
|
|
203
|
+
For isolated registries or different prefixes:
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
import { SuperLocalStorage } from "@titanpl/super-ls";
|
|
207
|
+
|
|
208
|
+
const gameStorage = new SuperLocalStorage("game_");
|
|
209
|
+
const userStorage = new SuperLocalStorage("user_");
|
|
210
|
+
|
|
211
|
+
gameStorage.register(Player);
|
|
212
|
+
userStorage.register(Profile);
|
|
213
|
+
|
|
214
|
+
// Keys are prefixed automatically
|
|
215
|
+
gameStorage.set("hero", player); // Stored as "game_hero"
|
|
216
|
+
userStorage.set("current", profile); // Stored as "user_current"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## ๐ API Reference
|
|
222
|
+
|
|
223
|
+
### `superLs.set(key, value)`
|
|
224
|
+
|
|
225
|
+
Stores any JavaScript value in Titan storage.
|
|
226
|
+
|
|
227
|
+
| Parameter | Type | Description |
|
|
228
|
+
|-----------|------|-------------|
|
|
229
|
+
| `key` | `string` | Storage key |
|
|
230
|
+
| `value` | `any` | Data to store |
|
|
231
|
+
|
|
232
|
+
**Supported types**: primitives, objects, arrays, `Map`, `Set`, `Date`, `RegExp`, `BigInt`, `TypedArray`, `undefined`, `NaN`, `Infinity`, circular references, registered class instances.
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
superLs.set("config", { theme: "dark", items: new Set([1, 2, 3]) });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### `superLs.get(key)`
|
|
239
|
+
|
|
240
|
+
Retrieves and deserializes a value with full type restoration.
|
|
241
|
+
|
|
242
|
+
| Parameter | Type | Description |
|
|
243
|
+
|-----------|------|-------------|
|
|
244
|
+
| `key` | `string` | Storage key |
|
|
245
|
+
| **Returns** | `any \| null` | Restored value or `null` if not found |
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
const config = superLs.get("config");
|
|
249
|
+
if (config) {
|
|
250
|
+
console.log(config.items instanceof Set); // true
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### `superLs.register(ClassRef, typeName?)`
|
|
255
|
+
|
|
256
|
+
Registers a class for automatic serialization/deserialization.
|
|
257
|
+
|
|
258
|
+
| Parameter | Type | Description |
|
|
259
|
+
|-----------|------|-------------|
|
|
260
|
+
| `ClassRef` | `Function` | Class constructor |
|
|
261
|
+
| `typeName` | `string?` | Custom type name (defaults to `ClassRef.name`) |
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
superLs.register(Player);
|
|
265
|
+
superLs.register(Enemy, "GameEnemy"); // Custom name
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### `new SuperLocalStorage(prefix?)`
|
|
269
|
+
|
|
270
|
+
Creates a new storage instance with isolated registry.
|
|
271
|
+
|
|
272
|
+
| Parameter | Type | Default | Description |
|
|
273
|
+
|-----------|------|---------|-------------|
|
|
274
|
+
| `prefix` | `string` | `"sls_"` | Key prefix for all operations |
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
import { SuperLocalStorage } from "@titanpl/super-ls";
|
|
278
|
+
const custom = new SuperLocalStorage("myapp_");
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## ๐ฏ When to Use Static `hydrate()` Method
|
|
284
|
+
|
|
285
|
+
By default, `super-ls` reconstructs class instances like this:
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
const instance = new Constructor(); // Calls constructor WITHOUT arguments
|
|
289
|
+
Object.assign(instance, data); // Copies properties
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
This works **only if** your constructor can be called without arguments:
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
// โ
WORKS - has default values
|
|
296
|
+
class Player {
|
|
297
|
+
constructor(name = '', score = 0) {
|
|
298
|
+
this.name = name;
|
|
299
|
+
this.score = score;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
But **fails** if constructor requires arguments:
|
|
305
|
+
|
|
306
|
+
```javascript
|
|
307
|
+
// โ FAILS - required arguments
|
|
308
|
+
class Player {
|
|
309
|
+
constructor(name, score) {
|
|
310
|
+
if (!name) throw new Error('Name is required!');
|
|
311
|
+
this.name = name;
|
|
312
|
+
this.score = score;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// super-ls tries: new Player() โ ๐ฅ Error!
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### The Solution
|
|
320
|
+
|
|
321
|
+
Define a static `hydrate()` method that tells `super-ls` how to reconstruct your class:
|
|
322
|
+
|
|
323
|
+
```javascript
|
|
324
|
+
class Player {
|
|
325
|
+
constructor(name, score) {
|
|
326
|
+
if (!name) throw new Error('Name is required!');
|
|
327
|
+
this.name = name;
|
|
328
|
+
this.score = score;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
static hydrate(data) {
|
|
332
|
+
return new Player(data.name, data.score);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Quick Reference
|
|
338
|
+
|
|
339
|
+
| Constructor Style | Needs `hydrate()`? | Example |
|
|
340
|
+
|-------------------|-------------------|---------|
|
|
341
|
+
| All params have defaults | โ No | `constructor(name = '', score = 0)` |
|
|
342
|
+
| No parameters | โ No | `constructor()` |
|
|
343
|
+
| Required parameters | โ
Yes | `constructor(name, score)` |
|
|
344
|
+
| Has validation | โ
Yes | `if (!name) throw new Error()` |
|
|
345
|
+
| Uses `Object.freeze()` | โ
Yes | `Object.freeze(this)` |
|
|
346
|
+
| Private fields (`#prop`) | โ
Yes | `this.#secret = value` |
|
|
347
|
+
| Destructuring params | โ
Yes | `constructor({ name, score })` |
|
|
348
|
+
|
|
349
|
+
### Examples
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
// โ
NO hydrate needed - has defaults
|
|
353
|
+
class Counter {
|
|
354
|
+
constructor(value = 0) {
|
|
355
|
+
this.value = value;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// โ NEEDS hydrate - required params
|
|
360
|
+
class Email {
|
|
361
|
+
constructor(value) {
|
|
362
|
+
if (!value.includes('@')) throw new Error('Invalid');
|
|
363
|
+
this.value = value;
|
|
364
|
+
}
|
|
365
|
+
static hydrate(data) {
|
|
366
|
+
return new Email(data.value);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// โ NEEDS hydrate - Object.freeze()
|
|
371
|
+
class ImmutableConfig {
|
|
372
|
+
constructor(settings) {
|
|
373
|
+
this.settings = settings;
|
|
374
|
+
Object.freeze(this);
|
|
375
|
+
}
|
|
376
|
+
static hydrate(data) {
|
|
377
|
+
return new ImmutableConfig(data.settings);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// โ NEEDS hydrate - destructuring
|
|
382
|
+
class Player {
|
|
383
|
+
constructor({ name, score }) {
|
|
384
|
+
this.name = name;
|
|
385
|
+
this.score = score;
|
|
386
|
+
}
|
|
387
|
+
static hydrate(data) {
|
|
388
|
+
return new Player({ name: data.name, score: data.score });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## โ ๏ธ Known Limitations
|
|
396
|
+
|
|
397
|
+
| Limitation | Behavior | Workaround |
|
|
398
|
+
|------------|----------|------------|
|
|
399
|
+
| **Functions** | Throws error | Store function results, not functions |
|
|
400
|
+
| **WeakMap / WeakSet** | Silently becomes `{}` | Use `Map` / `Set` instead |
|
|
401
|
+
| **Symbol properties** | Not serialized | Use string keys |
|
|
402
|
+
| **Sparse arrays** | Holes become `undefined` | Use dense arrays or objects |
|
|
403
|
+
| **Unregistered classes** | Become plain objects (methods lost) | Register all classes |
|
|
404
|
+
| **Getters/Setters** | Not serialized as values | Work via prototype after restoration |
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## ๐ง Under the Hood
|
|
409
|
+
|
|
410
|
+
`super-ls` uses a two-phase transformation:
|
|
411
|
+
|
|
412
|
+
### Serialization (`set`)
|
|
413
|
+
1. Recursively traverse the value
|
|
414
|
+
2. Wrap registered class instances with type metadata (`__super_type__`, `__data__`)
|
|
415
|
+
3. Track circular references via `WeakMap`
|
|
416
|
+
4. Serialize using `devalue` (handles `Map`, `Set`, `Date`, etc.)
|
|
417
|
+
5. Store string in `t.ls`
|
|
418
|
+
|
|
419
|
+
### Deserialization (`get`)
|
|
420
|
+
1. Parse string using `devalue`
|
|
421
|
+
2. Recursively traverse parsed data
|
|
422
|
+
3. Detect type metadata and restore class instances
|
|
423
|
+
4. Create instance using:
|
|
424
|
+
- `hydrate()` if available
|
|
425
|
+
- Otherwise: `new Constructor()` + `Object.assign()`
|
|
426
|
+
5. Preserve circular references via placeholder morphing
|
|
427
|
+
|
|
428
|
+
For detailed technical documentation, see [EXPLAIN.md](./EXPLAIN.md).
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## ๐งช Testing
|
|
433
|
+
|
|
434
|
+
The library includes comprehensive test suites:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
# Install dependencies
|
|
438
|
+
npm install
|
|
439
|
+
|
|
440
|
+
# Run all tests
|
|
441
|
+
npm test
|
|
442
|
+
|
|
443
|
+
# Run with coverage
|
|
444
|
+
npm run test:coverage
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Test Coverage**: 73 tests across 2 suites
|
|
448
|
+
- Normal cases: 36 tests (basic types, class hydration, DI patterns)
|
|
449
|
+
- Edge cases: 37 tests (inheritance, circular refs, stress tests)
|
|
450
|
+
|
|
451
|
+
See [TEST_DOCUMENTATION.md](./TEST_DOCUMENTATION.md) for detailed test descriptions.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## ๐ Project Structure
|
|
456
|
+
|
|
457
|
+
```
|
|
458
|
+
super-ls/
|
|
459
|
+
โโโ index.js # Main implementation
|
|
460
|
+
โโโ index.d.ts # TypeScript definitions
|
|
461
|
+
โโโ package.json
|
|
462
|
+
โโโ README.md # This file
|
|
463
|
+
โโโ EXPLAIN.md # Technical deep-dive
|
|
464
|
+
โโโ TEST_DOCUMENTATION.md # Test suite documentation
|
|
465
|
+
โโโ tests/
|
|
466
|
+
โโโ super-ls.normal-cases.spec.js
|
|
467
|
+
โโโ super-ls.edge-cases.spec.js
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## ๐ค Contributing
|
|
473
|
+
|
|
474
|
+
Contributions are welcome! Please:
|
|
475
|
+
|
|
476
|
+
1. Fork the repository
|
|
477
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
478
|
+
3. Write tests for new functionality
|
|
479
|
+
4. Ensure all tests pass (`npm test`)
|
|
480
|
+
5. Submit a Pull Request
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## ๐ License
|
|
485
|
+
|
|
486
|
+
ISC ยฉ Titan Planet
|