@nativelayer.dev/restate 0.2.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.

Potentially problematic release.


This version of @nativelayer.dev/restate might be problematic. Click here for more details.

package/README.md ADDED
@@ -0,0 +1,1349 @@
1
+ ![restate Logo](https://raw.githubusercontent.com/ynck-chrl/restate/main/restate-logo.jpg)
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@nativelayer.dev/restate.svg)](https://www.npmjs.com/package/@nativelayer.dev/restate)
4
+ [![License](https://img.shields.io/badge/license-PolyForm%20NC%201.0.0-blue.svg)](./LICENSE)
5
+ ![Bundle size](https://img.shields.io/badge/minified-~12.3KB-green.svg)
6
+
7
+ # restate
8
+
9
+ version `0.2.0`
10
+
11
+ A minimal, framework-agnostic reactive state management library built on ES6 proxies. `restate` provides:
12
+
13
+ - A tiny footprint (~2.8 KB gzipped), zero dependencies.
14
+ - A simple core API: `$onChange`, `$watch`, `$set`, `$destroy`, plus built-in `$computed` and `$methods`.
15
+ - Chainable plugin system to add persistence, history, immutability, async handling, and validation.
16
+ - Both CommonJS and ES module builds for universal compatibility.
17
+ - No boilerplate: read and write state directly on a proxy.
18
+
19
+ **Bundle size (minified + gzipped): ~2.8 KB** — includes built-in `$computed()` and `$methods()`.
20
+
21
+
22
+ ## Installation
23
+
24
+ Import directly in a browser or module bundler:
25
+
26
+ ```js
27
+ // ESM (recommended)
28
+ import { restate } from './dist/esm/restate.min.js';
29
+
30
+ // CommonJS (Node.js)
31
+ const { restate } = require('./dist/cjs/restate.min.js');
32
+ ```
33
+
34
+ ### Which format should I use?
35
+
36
+ | Your setup | Use this |
37
+ |------------|----------|
38
+ | Vite, esbuild, modern bundler | `dist/esm/` (recommended) |
39
+ | Node.js with `require()` | `dist/cjs/` |
40
+ | Node.js with `"type": "module"` | `dist/esm/` |
41
+
42
+ ## Basic Usage
43
+
44
+ `restate` was designed to have a fast learning curve and to provide pleasant developer experience.
45
+ Basic operations can be done with a straight-forward syntax:
46
+
47
+ ### Create a Reactive State
48
+
49
+ Initialize with a plain object:
50
+
51
+ ```js
52
+ const state = restate({
53
+ count: 0,
54
+ items: []
55
+ });
56
+ ```
57
+
58
+ ### Access and Mutation
59
+
60
+ Read and write properties directly on the reactive proxy:
61
+
62
+ ```js
63
+ console.log(state.count); // 0
64
+ // Straight forward syntax for mutations
65
+ state.count = 5; // mutates 'count'
66
+ // and triggers change notifications
67
+ ```
68
+
69
+ ## Core Reactivity API
70
+
71
+ ### $onChange(fn)
72
+
73
+ Subscribe to *all* key/value changes. The callback receives `(key, newValue, oldValue)`:
74
+
75
+ ```js
76
+ state.$onChange((key, newValue, oldValue) => {
77
+ console.log(`${key} changed from`, oldValue, 'to', newValue);
78
+ });
79
+
80
+ // You can also omit oldValue if you’re only interested in the new value:
81
+ state.$onChange((key, newValue) => {
82
+ console.log(`${key} changed to`, newValue);
83
+ });
84
+ ```
85
+
86
+ ### $watch(path, handler)
87
+
88
+ Listen to specific key or wildcard patterns (`\*` or `\**`). The handler receives `(key, newValue, oldValue)`:
89
+
90
+ ```js
91
+ // Exact key
92
+ state.$watch('count', (key, newValue, oldValue) => console.log(`count changed from ${oldValue} to ${newValue}`));
93
+ // You can omit oldValue if desired:
94
+ state.$watch('count', (key, newValue) => console.log(`count is now ${newValue}`));
95
+
96
+ // Single-level wildcard: watches direct children of 'user'
97
+ state.$watch('user.*', (key, newValue, oldValue) => console.log(key, newValue, oldValue));
98
+
99
+ // Deep wildcard: watches all nested descendants of 'config'
100
+ state.$watch('config.**', (key, newValue, oldValue) => console.log(key, newValue, oldValue));
101
+ ```
102
+
103
+ Returns an *unwatch* function to cancel:
104
+
105
+ ```js
106
+ const unwatch = state.$watch('count', handler);
107
+ unwatch();
108
+ ```
109
+
110
+ ### $set(updatesOrFn)
111
+
112
+ Batch multiple updates into one operation :
113
+
114
+ ```js
115
+ // Object form
116
+ state.$set({ count: 10, foo: 'bar' });
117
+
118
+ // Function form
119
+ state.$set(s => {
120
+ s.count++;
121
+ s.items.push('x');
122
+ });
123
+
124
+ // Nested object update
125
+ state.$set({
126
+ user: { name: 'Alice', roles: ['admin', 'editor'] },
127
+ loggedIn: true
128
+ });
129
+ console.log(state.user); // { name: 'Alice', roles: ['admin','editor'] }
130
+ console.log(state.loggedIn); // true
131
+
132
+ // Complex updates with function form
133
+ state.$set(s => {
134
+ // toggle a boolean flag
135
+ s.loggedIn = !s.loggedIn;
136
+ // increment count by 5 only if logged in
137
+ if (s.loggedIn) {
138
+ s.count += 5;
139
+ }
140
+ });
141
+ console.log(state.count, state.loggedIn);
142
+ ```
143
+
144
+ ### $methods(obj)
145
+
146
+ Define custom methods on the state proxy. Methods are **non-enumerable** and have `this` bound to the proxy:
147
+
148
+ ```js
149
+ state.$methods({
150
+ increment() { this.count++; },
151
+ reset() { this.count = 0; }
152
+ });
153
+
154
+ state.increment(); // count = 1
155
+ state.reset(); // count = 0
156
+ ```
157
+
158
+ > Why use `$methods()` instead of direct assignment?
159
+
160
+ ```js
161
+ // ❌ Direct assignment - problematic
162
+ state.getUserById = function(id) { return this.users.find(u => u.id === id); };
163
+ ```
164
+
165
+ | Issue | Direct Assignment | `$methods()` |
166
+ |-------|-------------------|--------------|
167
+ | **Triggers watchers** | ✅ Yes (unwanted) | ❌ No |
168
+ | **Enumerable** | ✅ Yes (pollutes `Object.keys()`) | ❌ No |
169
+ | **`this` binding** | ⚠️ Can be lost | ✅ Always bound |
170
+ | **Shows in `JSON.stringify`** | ⚠️ Attempted | ❌ No |
171
+
172
+ **Use `$methods()` to cleanly separate behavior from data.**
173
+
174
+ Usage:
175
+
176
+ ```js
177
+ // $methods() is available without any plugin
178
+ state.$methods({
179
+ increment(n) { this.count += n; },
180
+ reset() { this.count = 0; }
181
+ });
182
+ ```
183
+
184
+ Custom methods are added to the reactive proxy (non-enumerable) and can be called directly:
185
+
186
+ ```js
187
+ state.increment(5);
188
+ state.reset();
189
+ ```
190
+
191
+ ### $destroy()
192
+
193
+ Disable further updates and clear watchers:
194
+
195
+ ```js
196
+ state.$destroy();
197
+ state.count = 7; // Throws or no-op
198
+ ```
199
+
200
+ ### $computed(obj)
201
+
202
+ Define reactive computed properties with automatic dependency tracking:
203
+
204
+ ```js
205
+ import { restate } from './dist/esm/restate.min.js';
206
+
207
+ // Initialize state - $computed is built-in, no plugin needed
208
+ const state = restate({ a: 1, b: 2 });
209
+
210
+ // Define computed properties
211
+ state.$computed({
212
+ sum: s => {
213
+ console.log('computing sum');
214
+ return s.a + s.b;
215
+ },
216
+ double: s => s.sum * 2
217
+ });
218
+
219
+ // First access triggers computation
220
+ console.log(state.sum); // logs 'computing sum', outputs 3
221
+ console.log(state.double); // outputs 6 (sum is cached)
222
+
223
+ // Second access uses cache
224
+ console.log(state.sum); // no log, outputs 3
225
+
226
+ // Changing a base value invalidates cache
227
+ state.a = 5; // invalidates sum
228
+ console.log(state.sum); // logs 'computing sum', outputs 7
229
+ console.log(state.double); // logs no 'computing sum' but recomputes double: outputs 14
230
+ ```
231
+
232
+ Chaining computed values:
233
+
234
+ ```js
235
+ import { restate } from './dist/esm/restate.min.js';
236
+
237
+ // No plugin needed - $computed is built-in
238
+ const state = restate({ a: 2, b: 3 });
239
+
240
+ state.$computed({
241
+ sum: s => s.a + s.b,
242
+ product: s => s.a * s.b,
243
+ doubledSum: s => s.sum * 2,
244
+ sumPlusProd: s => s.sum + s.product
245
+ });
246
+
247
+ console.log(state.sum); // → 5
248
+ console.log(state.product); // → 6
249
+ console.log(state.doubledSum); // → 10
250
+ console.log(state.sumPlusProd); // → 11
251
+
252
+ state.a = 4; // invalidates `sum` & `product`
253
+ console.log(state.doubledSum); // → 14
254
+ ```
255
+
256
+ Computed over nested objects:
257
+
258
+ ```js
259
+ const userState = restate({ user: { first: 'Jane', last: 'Doe' } });
260
+
261
+ userState.$computed({
262
+ fullName: s => `${s.user.first} ${s.user.last}`,
263
+ initials: s => `${s.user.first[0]}.${s.user.last[0]}.`
264
+ });
265
+
266
+ console.log(userState.fullName); // → "Jane Doe"
267
+ console.log(userState.initials); // → "J.D."
268
+
269
+ userState.user.last = 'Smith';
270
+ console.log(userState.fullName); // → "Jane Smith"
271
+ ```
272
+
273
+ Reactive side-effects with `$onChange` + computed
274
+
275
+ ```js
276
+ const counter = restate({ count: 0 });
277
+
278
+ counter.$computed({
279
+ parity: s => s.count % 2 === 0 ? 'even' : 'odd'
280
+ });
281
+
282
+ counter.$onChange((key, newVal) => {
283
+ if (key === 'count') {
284
+ console.log(`Count is ${newVal}, parity: ${counter.parity}`);
285
+ }
286
+ });
287
+
288
+ counter.count = 1; // logs "Count is 1, parity: odd"
289
+ ```
290
+
291
+ Dynamically adding more computed props:
292
+
293
+ ```js
294
+ state.$computed({
295
+ triple: s => s.a * 3
296
+ });
297
+ console.log(state.triple); // → 12 (if a === 4)
298
+ ```
299
+
300
+ ### $dependencies()
301
+
302
+ Inspect dependency graph to know which properties each computed depends on (useful for debugging):
303
+
304
+ ```js
305
+ console.log(state.$dependencies());
306
+ // → {
307
+ // sum: ['a','b'],
308
+ // product: ['a','b'],
309
+ // doubledSum: ['sum'],
310
+ // sumPlusProd: ['sum','product']
311
+ // }
312
+ ```
313
+
314
+ Dependencies are automatically tracked; when a base property changes, computed caches are invalidated and recomputed on access.
315
+
316
+ ### All methods table
317
+
318
+ Below are the core methods available on every `restate` instance:
319
+
320
+ | Method | Parameters | Returns | Description |
321
+ |---------------------------------|-------------------------------------------------|----------------|-------------------------------------------------------------|
322
+ | `use(pluginFactory, options)` | `pluginFactory` (function or plugin), `options?` (object) | reactive proxy | Register a plugin after initialization |
323
+ | `$onChange(fn)` | `fn(key, newValue, oldValue)` (function) | reactive proxy | Subscribe to all value changes with previous value |
324
+ | `$watch(path, handler)` | `path` (string), `handler` (function) | `() => void` | Watch a specific path or wildcard (`\*`/`\**`) |
325
+ | `$watch(path, handler)` | `path` (string), `handler(key, newValue, oldValue)` (function) | `() => void` | Watch changes on a path or wildcard with previous value |
326
+ | `$unwatch(path)` | `path` (string) | reactive proxy | Remove the watcher for the given path |
327
+ | `$set(updatesOrFn)` | `updatesOrFn` (object or function) | reactive proxy | Apply atomic updates; accepts an object or updater function |
328
+ | `$computed(obj)` | `obj` (object of getter functions) | reactive proxy | Define computed properties with auto dependency tracking |
329
+ | `$dependencies()` | none | object | Get dependency graph for all computed properties |
330
+ | `$methods(obj)` | `obj` (object of functions) | reactive proxy | Define custom methods (non-enumerable, bound to proxy) |
331
+ | `$destroy()` | none | reactive proxy | Destroy the reactive proxy: clear watchers and disable updates |
332
+
333
+ All native methods combined:
334
+
335
+ ```js
336
+ // Example: core methods
337
+ const state = restate({ a: 1, b: 2, count: 0 });
338
+
339
+ // Change tracking
340
+ state.$onChange((k,v) => console.log(k, v));
341
+ const unwatch = state.$watch('a', (k,v) => console.log('watched', k, v));
342
+
343
+ // Computed properties (built-in)
344
+ state.$computed({
345
+ sum: s => s.a + s.b
346
+ });
347
+ console.log(state.sum); // 3
348
+
349
+ // Custom methods (built-in)
350
+ state.$methods({
351
+ increment() { this.count++; }
352
+ });
353
+ state.increment(); // count = 1
354
+
355
+ // Batch updates
356
+ state.$set({ a: 2 });
357
+
358
+ // Cleanup
359
+ unwatch();
360
+ state.$destroy();
361
+ ```
362
+
363
+ ## Plugin System
364
+
365
+ `retate` is designed with a plugin architecture to keep the core library minimal and focused, while providing extensibility for advanced use cases. Rather than bundling every possible feature into the main library (which would bloat the bundle size), restate exposes a plugin API that allows you to add only the functionality you need.
366
+
367
+ This design philosophy ensures that:
368
+
369
+ - **The core stays lightweight** (~2.8 KB) with zero dependencies
370
+ - **You only pay for what you use** — plugins are optional and can be loaded on demand
371
+ - **Custom behavior is possible** — you can build plugins tailored to your specific needs
372
+ - **The API remains simple** — core methods stay consistent regardless of which plugins are active
373
+
374
+ Plugins hook into `restate`'s lifecycle events (like `beforeSet`, `afterSet`, `beforeNotify`), enabling you to hook into state changes, add methods and integrate custom behaviors that allow to implement features like persistence, immutability, validation, history tracking, and more.
375
+
376
+ Chain optional plugins for additional features:
377
+
378
+ ```js
379
+ import { restate } from './dist/esm/restate.min.js';
380
+ import { PersistencePlugin } from './dist/esm/plugins/persistence.min.js';
381
+ import { ImmutablePlugin } from './dist/esm/plugins/immutable.min.js';
382
+
383
+ // Chainable registration:
384
+ const state = restate({ count: 0, items: [] })
385
+ .use(PersistencePlugin, { key: 'appState', debounce: 300 })
386
+ .use(ImmutablePlugin, { strict: false });
387
+ ```
388
+
389
+ ### Plugin Factory
390
+
391
+ To use a plugin call `state.use(pluginFactory, options)`. Under the hood it runs the `pluginFactory(option)` function:
392
+
393
+ ```js
394
+ const plugin = pluginFactory(options);
395
+ core.use(plugin);
396
+ ```
397
+
398
+ ### Plugin Object Shape
399
+
400
+ - `name` (string): Unique plugin identifier used in directives or logs.
401
+ - `hooks?` (object): Lifecycle hooks you can implement:
402
+ - `beforeSet(target, prop, value, path)`
403
+ - `afterSet(target, prop, value, path)`
404
+ - `beforeNotify(key, value)`
405
+ - `afterNotify(key, value)`
406
+ - `beforeBulk()`
407
+ - `afterBulk()`
408
+ - `onDestroy()`
409
+ - `methods?` (object): Custom methods to add to the reactive proxy:
410
+
411
+ ```js
412
+ methods: {
413
+ customAction() { /* this === reactive proxy */ }
414
+ }
415
+ ```
416
+
417
+ - `install?` (function): Runs once when the plugin is registered.
418
+
419
+ ### Example: Logging Plugin
420
+
421
+ ```js
422
+ function LogPlugin({ prefix = '' } = {}) {
423
+ return {
424
+ name: 'log',
425
+ hooks: {
426
+ afterNotify(key, value) {
427
+ console.log(`${prefix}${key} =>`, value);
428
+ }
429
+ }
430
+ };
431
+ }
432
+
433
+ const state = restate({ count: 0 })
434
+ .use(LogPlugin, { prefix: '[LOG] ' });
435
+ state.count = 1; // logs "[LOG] count => 1"
436
+ ```
437
+
438
+ ### Example: Reset Plugin
439
+
440
+ ```js
441
+ function ResetPlugin() {
442
+ return {
443
+ name: 'reset',
444
+ methods: {
445
+ reset() { this.count = 0; }
446
+ }
447
+ };
448
+ }
449
+
450
+ const state2 = restate({ count: 42 })
451
+ .use(ResetPlugin);
452
+ state2.reset();
453
+ console.log(state.count) // count is now 0
454
+ ```
455
+
456
+ You can freely mix built-in and custom plugins:
457
+
458
+ ```js
459
+ const state3 = restate({ count: 5 })
460
+ .use(PersistencePlugin, { key: 'c3' })
461
+ .use(LogPlugin)
462
+ .use(ResetPlugin);
463
+ ```
464
+
465
+ ---
466
+
467
+ ## Built-in Plugins
468
+
469
+ > **Note:** Built-in Plugins are audited and approuved by NativeWebDev team before being published. Always check and audit the code of Plugins you use with `restate` if they are not built-in plugins.
470
+
471
+ ### PersistencePlugin
472
+
473
+ Auto-saves and restores state to storage.
474
+
475
+ Usage:
476
+
477
+ ```js
478
+ state.use(PersistencePlugin, {
479
+ persist: true, // enable auto-persist (default)
480
+ persistKey: 'appState', // storage key (default 'restate')
481
+ persistStorage: 'localStorage', // or 'sessionStorage' or custom adapter
482
+ persistDebounce: 300, // ms debounce between saves
483
+ persistInclude: null, // array of paths to include
484
+ persistExclude: null, // array of paths to exclude
485
+ persistEncrypt: false, // password string for AES-256-GCM encryption
486
+ persistIntegrity: false, // enable HMAC-SHA256 integrity checking
487
+ persistVersion: 1, // version for migrations
488
+ persistMigrations: {}, // object mapping version→migrationFn
489
+ persistValidateSchema: null // (state) => boolean, validates before migrations
490
+ });
491
+ ```
492
+
493
+ | Option | Type | Default | Description |
494
+ |----------------------|-----------------------|---------------|-------------|
495
+ | persist | boolean | true | Enable auto-persist |
496
+ | persistKey | string | 'restate' | Storage key name |
497
+ | persistStorage | string \| object | 'localStorage' | Storage adapter |
498
+ | persistDebounce | number | 300 | ms between saves |
499
+ | persistInclude | array \| null | null | Paths to include |
500
+ | persistExclude | array \| null | null | Paths to exclude |
501
+ | persistEncrypt | boolean \| string | false | Password for AES-256-GCM |
502
+ | persistIntegrity | boolean \| string | false | Enable HMAC verification |
503
+ | persistVersion | number | 1 | Schema version |
504
+ | persistMigrations | object | {} | Version→migration map |
505
+ | persistValidateSchema| function \| null | null | Schema validator |
506
+
507
+ Hooks:
508
+
509
+ - `afterNotify` → triggers debounced save
510
+ - `onDestroy` → clears timers and pending saves
511
+
512
+ ---
513
+
514
+ ### ImmutablePlugin
515
+
516
+ Enforces immutability on direct property sets (optional bulk updates).
517
+
518
+ Usage:
519
+
520
+ ```js
521
+ state.use(ImmutablePlugin, {
522
+ strict: true // throw on direct sets (default)
523
+ });
524
+ ```
525
+
526
+ | Plugin Option | Type | Default |
527
+ |--------------------|-------------------|-----------|
528
+ | strict | boolean | true |
529
+ | customErrorMessage | function \| null | null |
530
+
531
+ #### Non-Strict Mode
532
+
533
+ By setting `strict: false`, direct property mutations are allowed without throwing:
534
+
535
+ ```js
536
+ import { ImmutablePlugin } from './dist/esm/plugins/immutable.min.js';
537
+ const state = restate({ count: 0, user: { name: 'Alice' } })
538
+ .use(ImmutablePlugin, { strict: false });
539
+
540
+ state.count = 1; // works without error
541
+ state.user.name = 'Bob'; // works without error
542
+ ```
543
+
544
+ You can also toggle strict mode at runtime using the plugin methods:
545
+
546
+ ```js
547
+ state.$enableImmutable(); // turn strict mode on
548
+ state.count = 2; // throws error
549
+
550
+ state.$disableImmutable(); // turn strict mode off
551
+ state.count = 3; // works without error
552
+ ```
553
+
554
+ | ImmutablePlugin Method | Type | Default |
555
+ |--------------------|-------------------|-----------|
556
+ | $enableImmutable | boolean | true |
557
+ | $disableImmutable$ | function \| null | null |
558
+
559
+ #### Strict Mode
560
+
561
+ With `strict: true`, direct property mutations throw errors, but you can still apply updates using `$set`:
562
+
563
+ ```js
564
+ import { ImmutablePlugin } from './dist/esm/plugins/immutable.min.js';
565
+ const state = restate({ count: 0 })
566
+ .use(ImmutablePlugin, { strict: true });
567
+
568
+ // Direct set throws
569
+ try { state.count = 1; } catch (e) { console.error(e.message); }
570
+
571
+ // update with $set works
572
+ state.$set({ count: 1 });
573
+ console.log(state.count); // 1
574
+ ```
575
+
576
+ #### Interoperability with `$methods()`
577
+
578
+ You can register custom mutation methods under strict immutability by using `$set` within your methods:
579
+
580
+ ```js
581
+ import { restate } from './dist/esm/restate.min.js';
582
+ import { ImmutablePlugin } from './dist/esm/plugins/immutable.min.js';
583
+
584
+ const state = restate({ count: 0 })
585
+ .use(ImmutablePlugin, { strict: true });
586
+
587
+ // $methods is built-in - define methods that use $set internally
588
+ state.$methods({
589
+ inc() {
590
+ this.$set({ count: this.count + 1 });
591
+ },
592
+ reset() {
593
+ this.$set({ count: 0 });
594
+ }
595
+ });
596
+
597
+ state.inc(); // count = 1
598
+ state.reset(); // count = 0
599
+ ```
600
+
601
+ ---
602
+
603
+ ### HistoryPlugin
604
+
605
+ Tracks state changes and provides undo/redo functionality.
606
+
607
+ Usage:
608
+
609
+ ```js
610
+ state.use(HistoryPlugin, {
611
+ trackHistory: true, // default
612
+ maxHistory: 50 // max snapshots to keep
613
+ });
614
+ ```
615
+
616
+ | Option | Type | Default |
617
+ |--------------|---------|---------|
618
+ | trackHistory | boolean | true |
619
+ | maxHistory | number | 50 |
620
+
621
+ Methods of HistoryPlugin:
622
+
623
+ | Method | Parameters | Returns | Description |
624
+ |------------------|------------|-------------|-------------------------------------|
625
+ | `$undo()` | None | state proxy | Revert to previous snapshot |
626
+ | `$redo()` | None | state proxy | Advance to next snapshot |
627
+ | `$canUndo()` | None | boolean | Check availability of undo |
628
+ | `$canRedo()` | None | boolean | Check availability of redo |
629
+ | `$getHistory()` | None | object | Get history metadata and indices |
630
+ | `$clearHistory()`| None | state proxy | Clear history stack |
631
+
632
+ Hooks:
633
+
634
+ - `beforeSet`, `beforeBulk` → record history before mutation
635
+ - `onDestroy` → clear history
636
+
637
+ ```js
638
+ import { restate } from './dist/esm/restate.min.js';
639
+ import { HistoryPlugin } from './dist/esm/plugins/history.min.js';
640
+
641
+ // Initialize state with history tracking
642
+ const state = restate({ count: 0 })
643
+ .use(HistoryPlugin, { trackHistory: true, maxHistory: 10 });
644
+
645
+ // Make some changes
646
+ state.count = 1;
647
+ state.count = 2;
648
+ state.count = 3;
649
+
650
+ // Inspect history
651
+ console.log(state.$getHistory());
652
+ // { history: [...], currentIndex: 2, canUndo: true, canRedo: false }
653
+
654
+ // Undo and redo
655
+ state.$undo();
656
+ console.log(state.count); // 2
657
+ state.$undo();
658
+ console.log(state.count); // 1
659
+ state.$redo();
660
+ console.log(state.count); // 2
661
+
662
+ // Check availability
663
+ console.log(state.$canUndo()); // true
664
+ console.log(state.$canRedo()); // true
665
+
666
+ // Clear history if needed
667
+ state.$clearHistory();
668
+ console.log(state.$canUndo()); // false
669
+ ```
670
+
671
+ ```js
672
+ // Undo all changes
673
+ while(state.$canUndo()) { state.$undo(); }
674
+ console.log(state.count); // back to initial value
675
+
676
+ // Redo all changes
677
+ while(state.$canRedo()) { state.$redo(); }
678
+ console.log(state.count); // back to latest value
679
+
680
+ // Branching: new changes clear redo stack
681
+ state.count = 5;
682
+ state.$undo();
683
+ state.count = 20; // new branch
684
+ console.log(state.$canRedo()); // false
685
+
686
+ // Peek history details without mutating
687
+ const { history, currentIndex } = state.$getHistory();
688
+ console.log(`Step ${currentIndex+1} of ${history.length}`, history);
689
+ ```
690
+
691
+ ---
692
+
693
+ ### AsyncPlugin
694
+
695
+ Provides async operations, loading/error state, caching, and retry logic.
696
+
697
+ Usage:
698
+
699
+ ```js
700
+ // Basic usage (with safe defaults)
701
+ state.use(AsyncPlugin());
702
+
703
+ // With custom limits
704
+ state.use(AsyncPlugin({
705
+ maxRetries: 5, // Cap on retries (default: 10)
706
+ defaultTimeout: 10000, // 10s timeout (default: 30s)
707
+ maxCacheSize: 50, // Max cache entries (default: 100)
708
+ exponentialBackoff: true // Exponential backoff for retries (default: true)
709
+ }));
710
+
711
+ // Per-request options
712
+ await state.$async('data', fetchData, {
713
+ timeout: 5000, // 5s for this request (overrides default)
714
+ retries: 3, // Retries for this request (capped by maxRetries)
715
+ cache: true,
716
+ cacheTime: 60000
717
+ });
718
+ ```
719
+
720
+ Methods added to store:
721
+
722
+ - `$async(key, asyncFn, options)` → manage an async task
723
+ - `$clearCache(key?)` → clear cached async results
724
+ - `$isLoading(key)` → boolean loading state
725
+
726
+ `$async` options:
727
+
728
+ - `timeout` (ms) — request timeout (default: 30s)
729
+ - `cache` (bool) & `cacheTime` (ms)
730
+ - `dedupe` (bool) for concurrent calls
731
+ - `retries`, `retryDelay` — retry attempts (capped by plugin's `maxRetries`)
732
+ - `optimistic` initial value
733
+ - `loadingKey`, `errorKey`, `dataKey` strings
734
+
735
+ Example:
736
+
737
+ ```js
738
+ await state.$async('todos', () => fetchTodos(), {
739
+ cache: true,
740
+ cacheTime: 600000,
741
+ loadingKey: 'todosLoading',
742
+ errorKey: 'todosError'
743
+ });
744
+ ```
745
+
746
+ **Basic loading & error handling**
747
+
748
+ ```js
749
+ // Basic loading and error state
750
+ await state.$async('data', fetchData, {
751
+ loadingKey: 'loading',
752
+ errorKey: 'error'
753
+ });
754
+ console.log(state.loading); // false
755
+ console.log(state.error); // undefined
756
+ ```
757
+
758
+ **Optimistic update**
759
+
760
+ ```js
761
+ // Provide an optimistic initial value
762
+ await state.$async('count', () => api.getCount(), {
763
+ optimistic: 0,
764
+ dataKey: 'count'
765
+ });
766
+ console.log(state.count); // 0 then actual value
767
+ ```
768
+
769
+ **Retry logic**
770
+
771
+ ```js
772
+ // Retry on failure up to 3 times
773
+ await state.$async('fetchUser', () => fetchUser(), {
774
+ retries: 3,
775
+ retryDelay: 1000,
776
+ dataKey: 'user',
777
+ errorKey: 'userError'
778
+ });
779
+ ```
780
+
781
+ **Dedupe concurrent calls**
782
+
783
+ ```js
784
+ // Dedupe concurrent invocations
785
+ const p1 = state.$async('todos', () => fetchTodos(), { dedupe: true });
786
+ const p2 = state.$async('todos', () => fetchTodosNew(), { dedupe: true });
787
+ console.log(p1 === p2); // true
788
+ ```
789
+
790
+ **Manual cache clear**
791
+
792
+ ```js
793
+ // Manually clear cached data
794
+ state.$clearCache('todos');
795
+ ```
796
+
797
+ **Loading state check**
798
+
799
+ ```js
800
+ // Check if async task is in progress
801
+ if (state.$isLoading('todos')) {
802
+ console.log('Todos are still loading...');
803
+ }
804
+ ```
805
+
806
+ ---
807
+
808
+ ### ValidatePlugin
809
+
810
+ Validate types or values on state updates using custom validator functions.
811
+
812
+ Usage:
813
+
814
+ ```js
815
+ import { restate } from './dist/esm/restate.min.js';
816
+ import { ValidatePlugin } from './dist/esm/plugins/validate.min.js';
817
+
818
+ const state = restate({ age: 0, name: '' })
819
+ .use(ValidatePlugin({
820
+ 'age': v => Number.isInteger(v) && v >= 0
821
+ }));
822
+ ```
823
+
824
+ Methods:
825
+
826
+ | Method | Parameters | Returns | Description |
827
+ |----------------------------|-------------------------------------|----------------|---------------------------------------|
828
+ | `$validators(newValidators)` | `object` mapping path→validator | reactive proxy | Add or update validators at runtime |
829
+
830
+ **Primitive validation**
831
+
832
+ ```js
833
+ // initial setup
834
+ const state = restate({ age: 0 }).use(ValidatePlugin({
835
+ 'age': v => Number.isInteger(v) && v >= 0
836
+ }));
837
+
838
+ state.age = 10; // valid
839
+ state.age = -5; // throws TypeError
840
+ ```
841
+
842
+ **Runtime validators**
843
+
844
+ ```js
845
+ state.$validators({
846
+ 'name': v => typeof v === 'string' && v.trim().length > 0
847
+ });
848
+
849
+ state.name = 'Alice'; // valid
850
+ state.name = ''; // throws TypeError
851
+ ```
852
+
853
+ **Nested property validation**
854
+
855
+ ```js
856
+ const userState = restate({ user: { email: '' } })
857
+ .use(ValidatePlugin({
858
+ 'user.email': v => /^[^@]+@[^@]+\.[^@]+$/.test(v)
859
+ }));
860
+
861
+ userState.user.email = 'bob@example.com'; // valid
862
+ userState.user.email = 'invalid-email'; // throws TypeError
863
+ ```
864
+
865
+ **Boolean validation**
866
+
867
+ ```js
868
+ const boolState = restate({ isAdmin: false })
869
+ .use(ValidatePlugin({
870
+ 'isAdmin': v => typeof v === 'boolean'
871
+ }));
872
+
873
+ boolState.isAdmin = true; // valid
874
+ boolState.isAdmin = 'yes'; // throws TypeError
875
+ ```
876
+
877
+ **Object validation**
878
+
879
+ ```js
880
+ const configState = restate({ config: {} })
881
+ .use(ValidatePlugin({
882
+ 'config': v =>
883
+ v && typeof v === 'object' && 'mode' in v && ['light','dark'].includes(v.mode)
884
+ }));
885
+
886
+ configState.config = { mode: 'dark' }; // valid
887
+ configState.config = { version: 1 }; // throws TypeError
888
+ ```
889
+
890
+ **Array validation**
891
+
892
+ ```js
893
+ const listState = restate({ items: [] })
894
+ .use(ValidatePlugin({
895
+ 'items': arr => Array.isArray(arr) && arr.every(x => typeof x === 'number')
896
+ }));
897
+
898
+ listState.items = [1,2,3]; // valid
899
+ listState.items = ['a','b','c']; // throws TypeError
900
+ ```
901
+
902
+
903
+ ## Snippets
904
+
905
+ ### Simple Persistence + Methods
906
+
907
+ ```js
908
+ import { restate } from './dist/esm/restate.min.js';
909
+ import { PersistencePlugin } from './dist/esm/plugins/persistence.min.js';
910
+
911
+ // Initialize with persistence ($methods is built-in, no plugin needed)
912
+ const state = restate({ count: 0, items: [] })
913
+ .use(PersistencePlugin, { persistKey: 'appState', persistDebounce: 0 });
914
+
915
+ // Define methods that use $set internally (built-in)
916
+ state.$methods({
917
+ addItem(item) {
918
+ this.$set({ items: [...this.items, item] });
919
+ },
920
+ increment() {
921
+ this.$set({ count: this.count + 1 });
922
+ }
923
+ });
924
+
925
+ // Use methods
926
+ state.increment();
927
+ state.addItem('apple');
928
+
929
+ console.log(state.count); // 1
930
+ console.log(state.items); // ['apple']
931
+
932
+ // On new session or reload, restore persisted state
933
+ const restored = restate({}).use(PersistencePlugin, { persistKey: 'appState', persistDebounce: 0 });
934
+ console.log(restored.count); // 1
935
+ console.log(restored.items); // ['apple']
936
+ ```
937
+
938
+ ---
939
+
940
+ ## Security Analysis
941
+
942
+ ### $computed (Built-in)
943
+
944
+ | Concern | Status | Description |
945
+ |---------|--------|-------------|
946
+ | Reserved key pollution | ✅ Fixed | Skips `__proto__`, `constructor`, `prototype` |
947
+ | Circular dependencies | ✅ Fixed | Detects and prevents infinite loops |
948
+ | Non-function values | ✅ Fixed | Validates that computed values are functions |
949
+ | Memory cleanup | ✅ OK | `$destroy()` clears all computed state |
950
+
951
+ **Protections implemented:**
952
+
953
+ ```javascript
954
+ // Reserved keys are skipped with warning
955
+ state.$computed({
956
+ __proto__: () => 'blocked', // ⚠️ Skipped
957
+ constructor: () => 'blocked', // ⚠️ Skipped
958
+ validKey: (s) => s.a + s.b // ✅ Allowed
959
+ });
960
+
961
+ // Circular dependencies detected
962
+ state.$computed({
963
+ a: (s) => s.b,
964
+ b: (s) => s.a // ❌ Error: Circular dependency detected
965
+ });
966
+
967
+ // Non-functions rejected
968
+ state.$computed({
969
+ notAFunction: 'string' // ⚠️ Skipped with warning
970
+ });
971
+ ```
972
+
973
+ ---
974
+
975
+ ### AsyncPlugin
976
+
977
+ | Concern | Status | Description |
978
+ | ------ | ---------- | ------------- |
979
+ | No timeouts | ✅ Fixed | Default 30s timeout, configurable per-request |
980
+ | Uncapped cache | ✅ Fixed | LRU eviction with `maxCacheSize` (default: 100) |
981
+ | Retry amplification | ✅ Fixed | Configurable `maxRetries` cap (default: 10) + exponential backoff |
982
+
983
+ **Plugin-level configuration:**
984
+
985
+ ```javascript
986
+ state.use(AsyncPlugin({
987
+ maxRetries: 10, // Cap on retries (prevents DoS)
988
+ defaultTimeout: 30000, // 30s default timeout
989
+ maxCacheSize: 100, // Max cache entries (LRU eviction)
990
+ exponentialBackoff: true // 1s, 2s, 4s, 8s... between retries
991
+ }));
992
+ ```
993
+
994
+ ### HistoryPlugin
995
+
996
+ | Concern | Status | Description |
997
+ | ------ | ---------- | ------------- |
998
+ | JSON.stringify comparison | ✅ Fixed | Replaced with `deepEqual()` function |
999
+ | Circular reference handling | ✅ Fixed | `deepClone()` and `deepEqual()` use WeakMap/WeakSet |
1000
+
1001
+ **Good:** Max history limit enforced, cleanup on destroy, circular refs supported.
1002
+
1003
+ ### ImmutablePlugin
1004
+
1005
+ | Concern | Status | Description |
1006
+ |------|----------|-------------|
1007
+ | Double proxy wrapping | ✅ Fixed | Now uses `beforeSet` hook, no extra proxy |
1008
+ | `_wrap` override conflicts | ✅ Fixed | No longer overrides `_wrap` |
1009
+
1010
+ **Good:** Uses hook system, proper strict mode, clear error messages, no conflicts with other plugins.
1011
+
1012
+ ### PersistencePlugin
1013
+
1014
+ | Risk | Severity | Status | Description |
1015
+ |------|----------|--------|-------------|
1016
+ | Plain text storage | Medium | ✅ Fixed | Sensitive data exposed in localStorage |
1017
+ | No integrity check | Low | ✅ Fixed | Stored data could be tampered |
1018
+ | Migration code execution | Low | ✅ Fixed | Custom migrations run user code |
1019
+
1020
+ **Fixes implemented:**
1021
+
1022
+ **1. Built-in AES-GCM encryption** — No more plain text storage:
1023
+
1024
+ ```javascript
1025
+ // Simple: just provide a password
1026
+ const state = restate({ secrets: {} })
1027
+ .use(PersistencePlugin({
1028
+ persistKey: 'myApp',
1029
+ persistEncrypt: 'my-secret-password' // AES-256-GCM encryption
1030
+ }));
1031
+ ```
1032
+
1033
+ **2. HMAC-SHA256 integrity checking** — Detect tampering:
1034
+
1035
+ ```javascript
1036
+ const state = restate({ data: {} })
1037
+ .use(PersistencePlugin({
1038
+ persistKey: 'myApp',
1039
+ persistEncrypt: 'password', // Encryption
1040
+ persistIntegrity: true // + HMAC verification
1041
+ }));
1042
+ // If localStorage is modified, loadState() returns false
1043
+ ```
1044
+
1045
+ **3. Schema validation before migrations** — Validate before executing user code:
1046
+
1047
+ ```javascript
1048
+ const state = restate({ user: { id: '', name: '' } })
1049
+ .use(PersistencePlugin({
1050
+ persistKey: 'myApp',
1051
+ persistVersion: 2,
1052
+ persistMigrations: {
1053
+ 2: (state) => ({ ...state, user: { ...state.user, email: '' } })
1054
+ },
1055
+ persistValidateSchema: (state) => {
1056
+ // Runs BEFORE migrations — reject malformed data
1057
+ return state && typeof state.user === 'object' && typeof state.user.id === 'string';
1058
+ }
1059
+ }));
1060
+ ```
1061
+
1062
+ **Good:** Encryption hooks available, debounced saves, path filtering, Web Crypto API (PBKDF2 key derivation).
1063
+
1064
+ ### ValidatePlugin
1065
+
1066
+ | Risk | Severity | Status | Description |
1067
+ |------|----------|--------|-------------|
1068
+ | Value in error message | Low | ✅ Fixed | `JSON.stringify(value)` could expose secrets |
1069
+ | Nested proxy overhead | Low | ✅ Fixed | Old implementation wrapped proxies with more proxies |
1070
+
1071
+ **1. Values are now safely redacted** in error messages to prevent sensitive data exposure:
1072
+
1073
+ **2. Now uses `beforeSet` hook** instead of monkey-patching `_wrap()` and creating nested proxies:
1074
+
1075
+ ```javascript
1076
+ // Old approach (problematic):
1077
+ // - Overrode core._wrap() method
1078
+ // - Wrapped core._proxy with another proxy in install()
1079
+ // - Double proxy overhead on every access
1080
+
1081
+ // New approach (clean):
1082
+ return {
1083
+ name: 'validation',
1084
+ hooks: {
1085
+ beforeSet(path, value, oldValue) {
1086
+ validateValue(path, value); // Throws TypeError if invalid
1087
+ }
1088
+ },
1089
+ methods: { $validators(v) { /* ... */ } }
1090
+ };
1091
+ ```
1092
+
1093
+ Error messages now safely redact sensitive values:
1094
+
1095
+ ```javascript
1096
+ // Before (exposed full value):
1097
+ // "Validation failed for 'user.password': invalid value "secretPassword123""
1098
+
1099
+ // After (redacted):
1100
+ // "Validation failed for 'user.password': received [string, 16 chars]"
1101
+ // "Validation failed for 'config': received [object, 5 keys]"
1102
+ // "Validation failed for 'items': received [array, 10 items]"
1103
+ ```
1104
+
1105
+ ---
1106
+
1107
+ ## Security Best Practices
1108
+
1109
+ > If you discover a security vulnerability, please report it privately to the maintainers rather than opening a public issue.
1110
+
1111
+ ### What is already protected
1112
+
1113
+ The core automatically blocks prototype pollution in these operations:
1114
+
1115
+ - `$set()` / `$set({ key: value })` — skips `__proto__`, `constructor`, `prototype`
1116
+ - `$computed()` — skips reserved keys
1117
+ - Direct property assignment — goes through Proxy (safe)
1118
+
1119
+ **You don't need to sanitize data for normal state operations:**
1120
+
1121
+ ```javascript
1122
+ // ✅ Safe - restate handles this internally
1123
+ state.$set({ user: apiResponse.user });
1124
+ state.config = userInput;
1125
+ ```
1126
+
1127
+ - ✅ No `eval()` or `Function()` constructor usage
1128
+ - ✅ No `innerHTML` or DOM manipulation
1129
+ - ✅ No network requests in core (AsyncPlugin is opt-in)
1130
+ - ✅ No file system access
1131
+ - ✅ No child process spawning
1132
+
1133
+ ---
1134
+
1135
+ ### When to sanitize
1136
+
1137
+ Sanitize at your application's **trust boundary** — where untrusted data enters your system:
1138
+
1139
+ ```javascript
1140
+ // Helper function (copy this if needed)
1141
+ const sanitize = (obj) => {
1142
+ if (typeof obj !== 'object' || obj === null) return obj;
1143
+ const clean = {};
1144
+ for (const [k, v] of Object.entries(obj)) {
1145
+ if (['__proto__', 'constructor', 'prototype'].includes(k)) continue;
1146
+ clean[k] = typeof v === 'object' ? sanitize(v) : v; // Recursive for nested
1147
+ }
1148
+ return clean;
1149
+ };
1150
+
1151
+ // ❌ DANGEROUS: Spreading untrusted data directly into objects
1152
+ const merged = { ...state.user, ...untrustedInput }; // Bypasses Proxy!
1153
+
1154
+ // ✅ SAFE: Sanitize before spreading
1155
+ const merged = { ...state.user, ...sanitize(untrustedInput) };
1156
+
1157
+ // ❌ DANGEROUS: Object.assign with untrusted data
1158
+ Object.assign(someObject, untrustedInput);
1159
+
1160
+ // ✅ SAFE: Sanitize first
1161
+ Object.assign(someObject, sanitize(untrustedInput));
1162
+ ```
1163
+
1164
+ ---
1165
+
1166
+ ### What operation to avoid
1167
+
1168
+ The following table lists all JavaScript operations that can bypass restate's Proxy traps:
1169
+
1170
+ | Operation | Bypasses Proxy? | Risk Level | Description | Safe Alternative |
1171
+ |-----------|-----------------|------------|-------------|------------------|
1172
+ | `{ ...obj }` (spread) | ✅ Yes | High | Creates plain object copy, no proxy traps | Use `state.$set()` or sanitize input |
1173
+ | `Object.assign(target, src)` | ✅ Yes | High | Copies properties directly to target | Sanitize source before assigning |
1174
+ | `JSON.parse(JSON.stringify(obj))` | ✅ Yes | High | Deep clone creates plain object | Use for serialization only, not mutation |
1175
+ | `structuredClone(obj)` | ✅ Yes | High | Deep clone bypasses proxy entirely | Use for cloning only, not mutation |
1176
+ | `Object.fromEntries(Object.entries(obj))` | ✅ Yes | Medium | Converts to/from entries, loses proxy | Avoid for state manipulation |
1177
+ | `_.cloneDeep(obj)` (Lodash) | ✅ Yes | High | Library clones create plain objects | Use native proxy-aware methods |
1178
+ | `Array.from(arr)` / `[...arr]` | ✅ Yes | Medium | Array spread creates plain array | Use `state.$set()` for array updates |
1179
+ | `.map()`, `.filter()`, `.slice()` | ⚠️ Partial | Low | Returns new plain array (reads are proxied) | Assign result back via `$set()` |
1180
+ | `Object.keys()` / `.values()` / `.entries()` | ❌ No | Low | Reads through proxy (safe for reading) | Safe to use |
1181
+ | `for...in` / `for...of` | ❌ No | Low | Iteration triggers proxy traps | Safe to use |
1182
+ | Destructuring `const { a } = state` | ⚠️ Partial | Low | Read is proxied, but `a` is now a plain value | Safe for reading, not for nested mutation |
1183
+ | `Object.getOwnPropertyDescriptor()` | ⚠️ Partial | Low | Can access descriptors directly | Avoid for untrusted keys |
1184
+ | `Reflect.get()` / `Reflect.set()` | ❌ No | Low | Works with proxy traps | Safe to use |
1185
+ | `delete obj.prop` | ❌ No | Low | Triggers proxy `deleteProperty` trap | Safe to use |
1186
+ | `'prop' in obj` | ❌ No | Low | Triggers proxy `has` trap | Safe to use |
1187
+
1188
+ **Legend:**
1189
+ - ✅ Yes = Completely bypasses proxy, creates unprotected data
1190
+ - ⚠️ Partial = Some operations are proxied, but result may not be
1191
+ - ❌ No = Works correctly with proxy traps
1192
+
1193
+ ```javascript
1194
+ // Examples of bypassing operations
1195
+
1196
+ // 1. Spread operator - creates plain object
1197
+ const copy = { ...state.user }; // copy is NOT a proxy
1198
+ copy.__proto__ = malicious; // ❌ No protection!
1199
+
1200
+ // 2. JSON round-trip - full bypass
1201
+ const clone = JSON.parse(JSON.stringify(state));
1202
+ clone.polluted = untrustedData; // ❌ No protection!
1203
+
1204
+ // 3. structuredClone - full bypass
1205
+ const deep = structuredClone(state);
1206
+ deep.__proto__ = malicious; // ❌ No protection!
1207
+
1208
+ // 4. Array methods return plain arrays
1209
+ const filtered = state.items.filter(x => x.active);
1210
+ filtered.push(untrustedItem); // ❌ No proxy protection
1211
+ state.$set({ items: filtered }); // ✅ Safe: goes through proxy
1212
+
1213
+ // 5. Destructuring extracts plain values
1214
+ const { user } = state; // user is now plain object if nested
1215
+ user.name = 'hacked'; // ⚠️ May not trigger watchers
1216
+
1217
+ // ✅ SAFE: Always use $set() for mutations
1218
+ state.$set({ user: { ...state.user, name: 'Alice' } });
1219
+ state.$set(s => { s.items.push(newItem); });
1220
+ ```
1221
+
1222
+ **Key insight:** The danger is when you use JavaScript operations (`...spread`, `Object.assign`, `JSON.parse`, `structuredClone`, etc.) that bypass restate's Proxy protection. If all data flows through `state.$set()` or direct property assignment on the proxy, you're protected.
1223
+
1224
+ ---
1225
+
1226
+ ### Encryption for persisted data
1227
+
1228
+ Choose your encryption key strategy based on your security needs:
1229
+
1230
+ ```javascript
1231
+ // 🟡 OBFUSCATION ONLY - Hardcoded key (visible in source code)
1232
+ // Use for: UI preferences, non-sensitive settings
1233
+ state.use(PersistencePlugin({
1234
+ persistEncrypt: 'app-obfuscation-key',
1235
+ persistIntegrity: true
1236
+ }));
1237
+ // ⚠️ Anyone viewing your bundle can extract this key
1238
+
1239
+ // 🟢 REAL SECURITY - User-derived key (recommended for sensitive data)
1240
+ // Use for: Auth tokens, personal data, sensitive information
1241
+ const userPassword = await promptUserForPassword();
1242
+ state.use(PersistencePlugin({
1243
+ persistEncrypt: userPassword, // Key derived via PBKDF2
1244
+ persistIntegrity: true
1245
+ }));
1246
+ // ✅ Only the user knows the key - truly encrypted
1247
+
1248
+ // 🟢 SESSION KEY - Server-provided (good with auth infrastructure)
1249
+ const { sessionKey } = await authenticatedFetch('/api/session-key');
1250
+ state.use(PersistencePlugin({
1251
+ persistEncrypt: sessionKey,
1252
+ persistIntegrity: true
1253
+ }));
1254
+ // ✅ Key not in source code, but requires backend support
1255
+ ```
1256
+
1257
+ | Key Strategy | Security Level | Best For |
1258
+ |--------------|----------------|----------|
1259
+ | Hardcoded / env var | 🟡 Obfuscation | UI state, preferences |
1260
+ | User-derived (password) | 🟢 Real encryption | Sensitive user data |
1261
+ | Server session key | 🟢 Good | Apps with auth backend |
1262
+
1263
+ ---
1264
+
1265
+ ### Validate all external inputs
1266
+
1267
+ ```javascript
1268
+ state.use(ValidatePlugin({
1269
+ 'user.email': v => /^[^@]+@[^@]+\.[^@]+$/.test(v),
1270
+ 'user.age': v => Number.isInteger(v) && v >= 0 && v <= 150
1271
+ }));
1272
+ ```
1273
+
1274
+ ---
1275
+
1276
+ ### Limit watcher scope
1277
+
1278
+ ```javascript
1279
+ state.$watch('user.profile.*', fn); // ✅ Specific paths
1280
+ // Avoid: state.$watch('**', fn); // ❌ Too broad, performance impact
1281
+ ```
1282
+
1283
+ ---
1284
+
1285
+ ### Set reasonable depth limits
1286
+
1287
+ ```javascript
1288
+ const state = restate(data, { maxDepth: 10 });
1289
+ ```
1290
+
1291
+ ## Libraries Comparison
1292
+
1293
+ | Library | Paradigm | Computed | Immutability | Key API | Size (gzipped) | Scope |
1294
+ |---------|----------|----------|--------------|---------|----------------|-------|
1295
+ | **restate** | Proxy + plugins | ✅ Built-in | Optional strict | `$set`, `$onChange`, `$watch`, `$computed`, `$methods` | ~2.8 KB core | Framework-agnostic |
1296
+ | Redux | Flux / functional | ❌ Needs reselect | Immutable by design | `createStore`, `dispatch`, `reducer` | ~2.5 KB + reselect ~0.6 KB | Framework-agnostic |
1297
+ | Vuex | Flux for Vue | ✅ Getters | Immutable via patterns | `state`, `getters`, `mutations`, `actions` | ~12 KB | Vue.js |
1298
+ | Zustand | Hooks | ⚠️ Manual | Mutable | `setState`, `getState`, `subscribe` | ~1.6 KB | React |
1299
+ | MobX | Observables | ✅ @computed | Mutable | Decorators, `action`, `autorun` | ~20 KB | Framework-agnostic |
1300
+ | XState | State machines | ❌ Derived only | Immutable transitions | `createMachine`, `interpret` | ~8 KB | Framework-agnostic |
1301
+
1302
+ All libraries have different trade-offs: `restate` provides fine-grained change tracking with built-in computed properties and custom methods, plus an extensible plugin system for additional features. It enables a middle ground between full immutability and direct mutation with minimal boilerplate.
1303
+
1304
+ ## License
1305
+
1306
+ `restate` is **dual-licensed** to support both open-source and commercial use.
1307
+
1308
+ ### Open-Source / Non-Commercial Use
1309
+
1310
+ `restate` is available under the **MIT License** for:
1311
+
1312
+ - ✅ Open-source projects (OSI-approved licenses)
1313
+ - ✅ Personal projects and non-revenue generating applications
1314
+ - ✅ Educational purposes and academic research
1315
+ - ✅ Non-profit organizations
1316
+ - ✅ Internal company tools (non-revenue generating)
1317
+ - ✅ Prototypes and MVPs
1318
+
1319
+ See [LICENSE](./LICENSE) and [LICENSE-NONCOMMERCIAL.md](./LICENSE-NONCOMMERCIAL.md) for full details.
1320
+
1321
+ ---
1322
+
1323
+ ### Commercial Use
1324
+
1325
+ A **commercial license** is required for:
1326
+
1327
+ - ❌ Proprietary software and closed-source commercial applications
1328
+ - ❌ SaaS products and revenue-generating applications
1329
+ - ❌ Enterprise deployments and large-scale corporate use
1330
+ - ❌ White-label products sold or licensed to third parties
1331
+
1332
+ **Commercial licenses include:**
1333
+
1334
+ - Legal protection and indemnification
1335
+ - Priority support and SLA
1336
+ - Updates and bug fixes during license term
1337
+ - Custom licensing terms for enterprise needs
1338
+
1339
+ See [EULA-COMMERCIAL.md](./EULA-COMMERCIAL.md) for commercial licensing terms.
1340
+
1341
+ ---
1342
+
1343
+ ### Licensing Inquiries
1344
+
1345
+ **For commercial licensing, pricing, or questions:**
1346
+
1347
+ > ynck.chrl@protonmail.com
1348
+
1349
+ I'll be happy to discuss licensing options that fit your needs.