memorio 4.7.1 → 4.8.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/README.md +447 -34
- package/SECURITY.md +10 -10
- package/SUMMARY.md +21 -0
- package/index.cjs +170 -31
- package/index.d.ts +4 -0
- package/index.js +125 -8
- package/llms.txt +119 -28
- package/markdown/CHANGELOG.md +8 -8
- package/markdown/HISTORY.md +192 -0
- package/markdown/INSPECT.md +116 -0
- package/markdown/MEMORY-ATTACHMENT.md +95 -0
- package/markdown/MEMORY.md +155 -0
- package/markdown/SCHEMA.md +169 -0
- package/markdown/SECURITY.md +1 -1
- package/markdown/SQLITE.md +181 -0
- package/markdown/SYNC.md +170 -0
- package/markdown/TYPED.md +158 -0
- package/package.json +15 -9
- package/types/exports.d.ts +27 -0
- package/types/history.d.ts +27 -0
- package/types/inspect.d.ts +14 -0
- package/types/memorio.d.ts +77 -2
- package/types/memory.d.ts +127 -0
- package/types/schema.d.ts +53 -0
- package/types/sqlite.d.ts +35 -0
package/llms.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
npm i memorio
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
> **For AI agents generating code against this library:** default `state` is a shared, global namespace unless a context is explicitly created with `memorio.createContext(id)`. Do not assume per-request or per-tab isolation is automatic
|
|
11
|
+
> **For AI agents generating code against this library:** default `state` is a shared, global namespace unless a context is explicitly created with `memorio.createContext(id)`. Do not assume per-request or per-tab isolation is automatic - see [Session & Context Isolation](#session--context-isolation) before generating server-side code that handles more than one user/request.
|
|
12
12
|
|
|
13
13
|
## Core Concepts
|
|
14
14
|
|
|
@@ -20,7 +20,7 @@ Memorio provides 6 storage modules plus utilities:
|
|
|
20
20
|
| `store` | localStorage persistence | Survives browser refresh; falls back to non-durable in-memory `Map` in Node.js/Deno |
|
|
21
21
|
| `session` | sessionStorage | Dies with browser tab; falls back to non-durable in-memory `Map` in Node.js/Deno |
|
|
22
22
|
| `cache` | In-memory cache | Fastest read, no persistence |
|
|
23
|
-
| `idb` | IndexedDB | Structured, async, persistent (browser-only
|
|
23
|
+
| `idb` | IndexedDB | Structured, async, persistent (browser-only - disabled in Node.js/Deno) |
|
|
24
24
|
| `observer` | Object watcher | Legacy; string-based paths, not statically checked against `state`'s shape |
|
|
25
25
|
| `useObserver` | React hook | Auto-discovery of state paths |
|
|
26
26
|
|
|
@@ -42,7 +42,7 @@ useObserver(
|
|
|
42
42
|
|
|
43
43
|
## API Reference
|
|
44
44
|
|
|
45
|
-
### `state`
|
|
45
|
+
### `state` - Reactive State
|
|
46
46
|
|
|
47
47
|
Global, Proxy-based, reactive state management.
|
|
48
48
|
|
|
@@ -65,21 +65,21 @@ state.remove('items')
|
|
|
65
65
|
state.removeAll()
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
**Locking
|
|
68
|
+
**Locking - VERIFY BEFORE PUBLISHING:** this library's own documents currently disagree on scope.
|
|
69
69
|
|
|
70
70
|
- One source describes **per-key** locking: `state.config.lock()` freezes only the `config` key; other keys remain writable.
|
|
71
71
|
- Another source describes **global** locking: `state.lock()` / `state.unlock()` freezing the entire `state` object at once.
|
|
72
72
|
|
|
73
|
-
These are materially different behaviors
|
|
73
|
+
These are materially different behaviors - confirm against the actual source which one (or both, with distinct method names) is implemented, then replace this note with the real signature(s). Do not ship a docs update, or generate code against this section, until this is resolved.
|
|
74
74
|
|
|
75
75
|
**Features:**
|
|
76
76
|
- Automatic path tracking via `__path` property
|
|
77
77
|
- Nested proxy support
|
|
78
78
|
- Auto-dispatches events on changes
|
|
79
79
|
|
|
80
|
-
### `store`
|
|
80
|
+
### `store` - localStorage Persistence
|
|
81
81
|
|
|
82
|
-
Persistent storage that survives browser refresh. On Node.js/Deno this falls back to an in-memory `Map` that does **not** survive a process restart
|
|
82
|
+
Persistent storage that survives browser refresh. On Node.js/Deno this falls back to an in-memory `Map` that does **not** survive a process restart - treat it as a same-shape cache there, not durable storage.
|
|
83
83
|
|
|
84
84
|
```javascript
|
|
85
85
|
// Set
|
|
@@ -109,9 +109,9 @@ console.debug(store.isPersistent) // true → real localStorage
|
|
|
109
109
|
await store.quota() // [usage, quota] in KB
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
> **`store.quota()` currently returns `[0, 0]` for the `localStorage` backend**
|
|
112
|
+
> **`store.quota()` currently returns `[0, 0]` for the `localStorage` backend** - it is not a real usage reading for that backend, it's a placeholder. Don't use it to make capacity decisions until it's implemented for `localStorage`; it may be meaningful for other backends (e.g. `idb`), but confirm before relying on it there too.
|
|
113
113
|
|
|
114
|
-
### `session`
|
|
114
|
+
### `session` - sessionStorage
|
|
115
115
|
|
|
116
116
|
Storage that dies when browser tab closes. Same Node.js/Deno `Map` fallback caveat as `store` applies here.
|
|
117
117
|
|
|
@@ -140,7 +140,7 @@ console.debug(session.size(), 'chars stored')
|
|
|
140
140
|
console.debug(session.isPersistent)
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
### `cache`
|
|
143
|
+
### `cache` - In-Memory Cache
|
|
144
144
|
|
|
145
145
|
Fastest possible read, data lost on refresh.
|
|
146
146
|
|
|
@@ -167,9 +167,9 @@ const value = cache['myKey']
|
|
|
167
167
|
delete cache['myKey']
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
-
### `idb`
|
|
170
|
+
### `idb` - IndexedDB
|
|
171
171
|
|
|
172
|
-
Structured, persistent, async database (browser-only). **Disabled in Node.js/Deno**
|
|
172
|
+
Structured, persistent, async database (browser-only). **Disabled in Node.js/Deno** - calls will warn and no-op; use `store` or `session` there instead.
|
|
173
173
|
|
|
174
174
|
```javascript
|
|
175
175
|
// Create database
|
|
@@ -206,9 +206,9 @@ if (idb.db.support()) {
|
|
|
206
206
|
}
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
Always guard `idb` calls with `idb.db.support()` (or `memorio.getCapabilities().hasIndexedDB`) in code that might run outside a browser
|
|
209
|
+
Always guard `idb` calls with `idb.db.support()` (or `memorio.getCapabilities().hasIndexedDB`) in code that might run outside a browser - don't rely on the no-op warning alone.
|
|
210
210
|
|
|
211
|
-
### `observer`
|
|
211
|
+
### `observer` - Object Watcher
|
|
212
212
|
|
|
213
213
|
Legacy observer API. Prefer `useObserver` in React code.
|
|
214
214
|
|
|
@@ -231,9 +231,9 @@ console.debug(observer.list)
|
|
|
231
231
|
observer.removeAll()
|
|
232
232
|
```
|
|
233
233
|
|
|
234
|
-
> Paths are plain strings and are not checked against `state`'s actual shape at compile time or at registration time. A typo or a later rename of the corresponding `state` key will fail silently
|
|
234
|
+
> Paths are plain strings and are not checked against `state`'s actual shape at compile time or at registration time. A typo or a later rename of the corresponding `state` key will fail silently - the observer simply never fires again.
|
|
235
235
|
|
|
236
|
-
### `useObserver`
|
|
236
|
+
### `useObserver` - React Hook
|
|
237
237
|
|
|
238
238
|
Primary way to observe state changes in React components.
|
|
239
239
|
|
|
@@ -264,7 +264,7 @@ function Counter() {
|
|
|
264
264
|
- Returns a cleanup function to stop monitoring
|
|
265
265
|
- Supports both Proxy objects and string paths
|
|
266
266
|
|
|
267
|
-
### `devtools`
|
|
267
|
+
### `devtools` - Inspection Tools
|
|
268
268
|
|
|
269
269
|
Browser-only (see [Cross-Platform Support](#cross-platform-support)); no-ops or unavailable in Node.js/Deno.
|
|
270
270
|
|
|
@@ -284,9 +284,9 @@ $session // globalThis.session
|
|
|
284
284
|
$cache // globalThis.cache
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
### `logger`
|
|
287
|
+
### `logger` - Change Tracking
|
|
288
288
|
|
|
289
|
-
Records every write it's configured to track, with timestamps
|
|
289
|
+
Records every write it's configured to track, with timestamps - including whatever values you pass in.
|
|
290
290
|
|
|
291
291
|
```javascript
|
|
292
292
|
memorio.logger.configure({
|
|
@@ -301,7 +301,7 @@ memorio.logger.clearHistory()
|
|
|
301
301
|
memorio.logger.exportLogs() // JSON string of all history
|
|
302
302
|
```
|
|
303
303
|
|
|
304
|
-
> The `value` field in each history entry is whatever was written
|
|
304
|
+
> The `value` field in each history entry is whatever was written - tokens, PII, anything. Don't enable `logger` unconditionally in production paths that handle sensitive data, and don't wire `exportLogs()` output anywhere it could leak (analytics, error reporters, support tooling) without redaction.
|
|
305
305
|
|
|
306
306
|
## Platform Detection
|
|
307
307
|
|
|
@@ -317,7 +317,7 @@ const caps = memorio.getCapabilities()
|
|
|
317
317
|
// { platform: 'browser', hasLocalStorage: true, hasIndexedDB: true, sessionId: 'uuid', ... }
|
|
318
318
|
```
|
|
319
319
|
|
|
320
|
-
Prefer checking `getCapabilities()` over assuming a platform from context
|
|
320
|
+
Prefer checking `getCapabilities()` over assuming a platform from context - especially before calling `idb` or relying on `store`/`session` durability.
|
|
321
321
|
|
|
322
322
|
## Cross-Platform Support
|
|
323
323
|
|
|
@@ -333,7 +333,7 @@ Prefer checking `getCapabilities()` over assuming a platform from context — es
|
|
|
333
333
|
|
|
334
334
|
## Session & Context Isolation
|
|
335
335
|
|
|
336
|
-
By default, `state` is a **shared global namespace**
|
|
336
|
+
By default, `state` is a **shared global namespace** - a value set in one place is visible everywhere else that reads `state` in the same process. There is no automatic per-tab or per-request isolation of `state` itself.
|
|
337
337
|
|
|
338
338
|
To isolate a slice of state (e.g. per tenant, per request), create an explicit context:
|
|
339
339
|
|
|
@@ -341,7 +341,7 @@ To isolate a slice of state (e.g. per tenant, per request), create an explicit c
|
|
|
341
341
|
const ctx = memorio.createContext('tenant-name')
|
|
342
342
|
ctx.state.user = { name: 'Isolated' }
|
|
343
343
|
|
|
344
|
-
console.debug(state.user) // undefined
|
|
344
|
+
console.debug(state.user) // undefined - separate namespace from ctx.state
|
|
345
345
|
|
|
346
346
|
memorio.listContexts()
|
|
347
347
|
memorio.deleteContext('context-id')
|
|
@@ -351,9 +351,100 @@ memorio.isolate('tenant-name') // alias for createContext
|
|
|
351
351
|
Isolation is implemented as a **key-prefix convention** inside the same underlying storage, not a hard memory or process boundary. In a shared Node.js process or an edge isolate that may be reused across requests:
|
|
352
352
|
|
|
353
353
|
- generate context IDs from trusted server-side data, never directly from client-controlled input, to prevent collisions or spoofing;
|
|
354
|
-
- don't treat this as your only isolation layer for data that must not cross tenants
|
|
354
|
+
- don't treat this as your only isolation layer for data that must not cross tenants - enforce that at the process/request level as well.
|
|
355
355
|
|
|
356
|
-
`getCapabilities().sessionId` provides a per-session identifier for browser contexts but is not itself an isolation mechanism
|
|
356
|
+
`getCapabilities().sessionId` provides a per-session identifier for browser contexts but is not itself an isolation mechanism - use `createContext` for that.
|
|
357
|
+
|
|
358
|
+
### `memorio.typed<T>()` - Typed Store (compile-time safety)
|
|
359
|
+
|
|
360
|
+
Returns the global `state` proxy cast to type `T`. The same Proxy instance — no overhead. Use for TypeScript autocomplete and static type checking.
|
|
361
|
+
|
|
362
|
+
```javascript
|
|
363
|
+
const app = memorio.typed<AppState>()
|
|
364
|
+
app.user = { name: 'Sara', age: 30 } // type-checked
|
|
365
|
+
app.user = { name: 42 } // ❌ compile error
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### `memorio.registerSchema()` - Schema Validation (runtime safety)
|
|
369
|
+
|
|
370
|
+
Register validators for state paths. Writes that violate a schema are rejected before being stored.
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
memorio.registerSchema('user', {
|
|
374
|
+
type: 'object',
|
|
375
|
+
required: ['name', 'email'],
|
|
376
|
+
properties: {
|
|
377
|
+
name: { type: 'string', min: 1 },
|
|
378
|
+
email: { type: 'string', pattern: /^[^@]+@[^@]+$/ }
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
state.user = { name: 'Sara' } // rejected: missing 'email'
|
|
383
|
+
state.user = { name: 'Sara', email: 'sara@test.com' } // accepted
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Custom validators:
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
memorio.registerSchema('theme', (val) =>
|
|
390
|
+
val === 'light' || val === 'dark' ? true : 'must be light or dark'
|
|
391
|
+
)
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Manual validation and management:
|
|
395
|
+
|
|
396
|
+
```javascript
|
|
397
|
+
memorio.validate('user', value) // { valid: true } or { valid: false, errors: [...] }
|
|
398
|
+
memorio.listSchemas() // ['user', 'theme']
|
|
399
|
+
memorio.unregisterSchema('theme') // removes validator
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Combine `typed<T>()` + `registerSchema()` for both compile-time and runtime safety.
|
|
403
|
+
|
|
404
|
+
### `memorio.snapshot()` / `memorio.diff()` - Time Travel
|
|
405
|
+
|
|
406
|
+
```javascript
|
|
407
|
+
memorio.enableHistory()
|
|
408
|
+
|
|
409
|
+
const snap = memorio.snapshot()
|
|
410
|
+
state.user.name = 'Luigi'
|
|
411
|
+
state.counter = 100
|
|
412
|
+
|
|
413
|
+
const changes = memorio.diff(snap)
|
|
414
|
+
// [{ path: 'user.name', oldValue: 'Sara', newValue: 'Luigi' }, ...]
|
|
415
|
+
|
|
416
|
+
memorio.rollback(snap) // restore to snapshot
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### `memorio.undo()` / `memorio.redo()` - Undo/Redo
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
state.a = 1; state.b = 2; state.c = 3
|
|
423
|
+
memorio.undo() // removes state.c
|
|
424
|
+
memorio.undo() // removes state.b
|
|
425
|
+
memorio.redo() // restores state.b
|
|
426
|
+
memorio.canUndo() // true
|
|
427
|
+
memorio.canRedo() // true
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### `memorio.trace()` - Mutation Log
|
|
431
|
+
|
|
432
|
+
```javascript
|
|
433
|
+
memorio.trace()
|
|
434
|
+
// [{ path, action, newValue, previousValue, timestamp }, ...]
|
|
435
|
+
memorio.clearHistory()
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### `memorio.stateKeys()` / `memorio.pathExists()` / `memorio.stateSchema()` - Introspection
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
memorio.stateKeys() // ['user', 'counter']
|
|
442
|
+
memorio.pathExists('user.name') // true or false
|
|
443
|
+
memorio.stateType('user.name') // 'string'
|
|
444
|
+
memorio.stateSchema() // [{ path, type, defined }, ...] - full tree report
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
357
448
|
|
|
358
449
|
## Security
|
|
359
450
|
|
|
@@ -361,9 +452,9 @@ Isolation is implemented as a **key-prefix convention** inside the same underlyi
|
|
|
361
452
|
- No `eval`, no dynamic code execution, no obfuscation, no hardcoded secrets.
|
|
362
453
|
- Inputs validated, keys sanitized before use.
|
|
363
454
|
- Secure random session IDs via `crypto.randomUUID`.
|
|
364
|
-
- Data in `store`, `session`, and `idb` is **not encrypted**
|
|
455
|
+
- Data in `store`, `session`, and `idb` is **not encrypted** - these are thin wrappers over browser storage APIs that persist data in the clear on the user's device. Add your own encryption layer before storing tokens, secrets, or regulated personal data there.
|
|
365
456
|
|
|
366
|
-
Engineering practices are informed by recognized guidance (e.g. NIST SP 800-53 practices) as a design input
|
|
457
|
+
Engineering practices are informed by recognized guidance (e.g. NIST SP 800-53 practices) as a design input - this is a statement about how the library is built, not a compliance certification, and no third-party audit has been performed. Report security issues privately (see `SECURITY.md`) rather than in a public issue.
|
|
367
458
|
|
|
368
459
|
## License
|
|
369
460
|
|
|
@@ -371,7 +462,7 @@ MIT © Dario Passariello (BigLogic Inc Canada)
|
|
|
371
462
|
|
|
372
463
|
## Utilities
|
|
373
464
|
|
|
374
|
-
### `memorio.dispatch`
|
|
465
|
+
### `memorio.dispatch` - Event Dispatch System
|
|
375
466
|
|
|
376
467
|
Internal event system used by state changes; also usable directly for custom events.
|
|
377
468
|
|
package/markdown/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## v4.6.1 (Security Patch) - 2026-08-14
|
|
7
|
+
## v4.6.1 (Security Patch) - 2026-08-14 - CRITICAL Security Fix
|
|
8
8
|
|
|
9
9
|
### 🔐 Security NOTICE (v4.6.0)
|
|
10
10
|
|
|
@@ -32,7 +32,7 @@ The hardcoded token `2f398d5d7a734781e96108fdd0dbbabad41ef77a` has been removed
|
|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
35
|
-
## v4.6.0 (Previous - SECURITY ISSUE) - 2026-08-13
|
|
35
|
+
## v4.6.0 (Previous - SECURITY ISSUE) - 2026-08-13 - Refactoring & Documentation
|
|
36
36
|
|
|
37
37
|
**⚠️ WARNING**: This version had a hardcoded PAT token that was later remediated in v4.6.1**
|
|
38
38
|
|
|
@@ -75,7 +75,7 @@ The hardcoded token `2f398d5d7a734781e96108fdd0dbbabad41ef77a` has been removed
|
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
-
## v3.0.2 (Current) - 2026-05-19
|
|
78
|
+
## v3.0.2 (Current) - 2026-05-19 - Bug Fix, Security & API Expansion
|
|
79
79
|
|
|
80
80
|
### 🐛 Bug Fixes
|
|
81
81
|
|
|
@@ -98,9 +98,9 @@ The hardcoded token `2f398d5d7a734781e96108fdd0dbbabad41ef77a` has been removed
|
|
|
98
98
|
|
|
99
99
|
- Added JSDoc to `observerFunction` in `functions/observer/index.ts`
|
|
100
100
|
- Added JSDoc to `cache` global in `functions/cache/index.ts`
|
|
101
|
-
- `lint` and `tsc` pass clean
|
|
101
|
+
- `lint` and `tsc` pass clean - 0 vulnerabilities from `npm audit`
|
|
102
102
|
|
|
103
|
-
### 🆕 API
|
|
103
|
+
### 🆕 API - New in 3.0.2
|
|
104
104
|
|
|
105
105
|
| Function | Description |
|
|
106
106
|
|----------|-------------|
|
|
@@ -136,10 +136,10 @@ The hardcoded token `2f398d5d7a734781e96108fdd0dbbabad41ef77a` has been removed
|
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
|
139
|
-
## v2.9.0
|
|
139
|
+
## v2.9.0 - 2026-05-13
|
|
140
140
|
|
|
141
141
|
### Added
|
|
142
|
-
- DevTools
|
|
142
|
+
- DevTools - `memorio.devtools.inspect()`, `stats()`, `exportData()`
|
|
143
143
|
- Logger with full history, stats and export
|
|
144
144
|
- Platform detection (`isBrowser`, `isNode`, `isDeno`, `isEdge`, `getCapabilities`)
|
|
145
145
|
- Session isolation via `crypto.randomUUID()`
|
|
@@ -154,7 +154,7 @@ The hardcoded token `2f398d5d7a734781e96108fdd0dbbabad41ef77a` has been removed
|
|
|
154
154
|
|
|
155
155
|
---
|
|
156
156
|
|
|
157
|
-
## v2.5.0
|
|
157
|
+
## v2.5.0 - 2026-02-17
|
|
158
158
|
|
|
159
159
|
- Initial release of memorio (state, store, session, cache, idb)
|
|
160
160
|
- Observer pattern (`observer`)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# History, Undo / Redo, Snapshot, Diff, Trace - Memorio
|
|
2
|
+
|
|
3
|
+
> ✅ **Universal**: Works in Browser, Node.js, Deno, and Edge Workers
|
|
4
|
+
|
|
5
|
+
Memorio provides a lightweight time-travel system for `state` mutations: snapshots, diffs, undo/redo, and a full mutation trace log.
|
|
6
|
+
|
|
7
|
+
History tracking is **opt-in** — it is disabled by default to avoid overhead. Enable it when you need undo/redo or trace capabilities.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Enable History
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import 'memorio'
|
|
15
|
+
|
|
16
|
+
memorio.enableHistory() // enable tracking
|
|
17
|
+
// ... mutate state ...
|
|
18
|
+
memorio.state.user = { name: 'Sara' }
|
|
19
|
+
memorio.state.counter = 42
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> Without `enableHistory()`, mutations are not recorded and `undo()`/`redo()`/`trace()` return empty results.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Snapshot & Diff
|
|
27
|
+
|
|
28
|
+
Snapshot captures the entire `state` tree at a point in time. Diff compares a snapshot against current state to see what changed.
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
// Enable history (snapshots work regardless, but trace/undo need it)
|
|
32
|
+
memorio.enableHistory()
|
|
33
|
+
|
|
34
|
+
// Take a snapshot
|
|
35
|
+
state.user = { name: 'Sara', age: 30 }
|
|
36
|
+
const snap = memorio.snapshot()
|
|
37
|
+
|
|
38
|
+
// Make changes
|
|
39
|
+
state.user.name = 'Luigi'
|
|
40
|
+
state.counter = 100
|
|
41
|
+
state.items = ['a', 'b']
|
|
42
|
+
|
|
43
|
+
// Diff against the snapshot
|
|
44
|
+
const changes = memorio.diff(snap)
|
|
45
|
+
console.debug(changes)
|
|
46
|
+
// [
|
|
47
|
+
// { path: 'user.name', oldValue: 'Sara', newValue: 'Luigi' },
|
|
48
|
+
// { path: 'counter', oldValue: undefined, newValue: 100 },
|
|
49
|
+
// { path: 'items', oldValue: undefined, newValue: ['a', 'b'] }
|
|
50
|
+
// ]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This is essential for AI agents: take a snapshot, make changes, inspect the diff, and decide whether to commit or rollback.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Undo / Redo
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
state.user = { name: 'Sara' }
|
|
61
|
+
state.counter = 100
|
|
62
|
+
state.items = ['a', 'b']
|
|
63
|
+
|
|
64
|
+
memorio.undo() // removes state.items
|
|
65
|
+
memorio.undo() // counter → undefined
|
|
66
|
+
memorio.redo() // counter → 100 again
|
|
67
|
+
|
|
68
|
+
memorio.canUndo() // true
|
|
69
|
+
memorio.canRedo() // true (after above undo + redo cycle)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- `undo()`: Restores the previous state by inverting the most recent mutation.
|
|
73
|
+
- `redo()`: Re-applies the most recently undone mutation.
|
|
74
|
+
- `canUndo()` / `canRedo()`: Check availability before calling.
|
|
75
|
+
|
|
76
|
+
### Max history depth
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
memorio.setMaxHistory(50) // keep at most 50 mutations per stack (default: 100)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Rollback (full state restore)
|
|
85
|
+
|
|
86
|
+
Unlike undo (which works one step at a time), `rollback` replaces the entire state from a snapshot:
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const snap = memorio.snapshot()
|
|
90
|
+
|
|
91
|
+
state.experiment = { result: 'failed' }
|
|
92
|
+
state.counter = 999
|
|
93
|
+
|
|
94
|
+
// Discard everything and restore to snapshot
|
|
95
|
+
memorio.rollback(snap)
|
|
96
|
+
// state.experiment is now gone
|
|
97
|
+
// state.counter is back to its snapshot value
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Trace (mutation log)
|
|
103
|
+
|
|
104
|
+
The trace log records every mutation with timestamp, path, action, and before/after values:
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
memorio.enableHistory()
|
|
108
|
+
|
|
109
|
+
state.user.name = 'Sara'
|
|
110
|
+
state.counter = 1
|
|
111
|
+
state.counter = 2
|
|
112
|
+
|
|
113
|
+
const log = memorio.trace()
|
|
114
|
+
console.debug(log)
|
|
115
|
+
// [
|
|
116
|
+
// { path: 'user.name', action: 'set', newValue: 'Sara', previousValue: undefined, timestamp: 1725... },
|
|
117
|
+
// { path: 'counter', action: 'set', newValue: 1, previousValue: undefined, timestamp: 1725... },
|
|
118
|
+
// { path: 'counter', action: 'set', newValue: 2, previousValue: 1, timestamp: 1725... }
|
|
119
|
+
// ]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
This is useful for:
|
|
123
|
+
- **AI debugging**: inspect what changed and when
|
|
124
|
+
- **Event sourcing**: export the log and replay state from scratch
|
|
125
|
+
- **Audit trails**: log all mutations for compliance
|
|
126
|
+
|
|
127
|
+
### Export / import trace
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
const log = memorio.trace()
|
|
131
|
+
localStorage.setItem('memorio-trace', JSON.stringify(log))
|
|
132
|
+
|
|
133
|
+
// Later, replay:
|
|
134
|
+
const saved = JSON.parse(localStorage.getItem('memorio-trace'))
|
|
135
|
+
for (const record of saved) {
|
|
136
|
+
if (record.action === 'set') {
|
|
137
|
+
state[record.path] = record.newValue
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Clear History
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
memorio.clearHistory() // wipe undo/redo stacks + trace log
|
|
148
|
+
memorio.clearRedo() // clear only the redo stack (undo stack preserved)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
> `clearHistory()` does NOT reset the current `state` — only the history tracking data.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Full API
|
|
156
|
+
|
|
157
|
+
| Method | Parameters | Returns | Description |
|
|
158
|
+
|--------|-----------|---------|-------------|
|
|
159
|
+
| `memorio.snapshot()` | none | `Record<string, any>` | Deep clone of current state |
|
|
160
|
+
| `memorio.diff(snap)` | `snap` | `DiffEntry[]` | Changed paths with old/new values |
|
|
161
|
+
| `memorio.undo()` | none | `MutationRecord \| undefined` | Undo last mutation |
|
|
162
|
+
| `memorio.redo()` | none | `MutationRecord \| undefined` | Redo last undone mutation |
|
|
163
|
+
| `memorio.canUndo()` | none | `boolean` | Whether undo is available |
|
|
164
|
+
| `memorio.canRedo()` | none | `boolean` | Whether redo is available |
|
|
165
|
+
| `memorio.rollback(snap)` | `snap` | `void` | Restore full state from snapshot |
|
|
166
|
+
| `memorio.trace()` | none | `MutationRecord[]` | List of all recorded mutations |
|
|
167
|
+
| `memorio.enableHistory(enabled?)` | `boolean` | `void` | Enable/disable tracking |
|
|
168
|
+
| `memorio.clearHistory()` | none | `void` | Clear all history stacks |
|
|
169
|
+
| `memorio.clearRedo()` | none | `void` | Clear only redo stack |
|
|
170
|
+
| `memorio.setMaxHistory(max)` | `number` | `void` | Set max stack depth |
|
|
171
|
+
| `memorio.getMaxHistory()` | none | `number` | Get current max depth |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## How It Works
|
|
176
|
+
|
|
177
|
+
1. When `historyEnabled` is true, the state proxy's callback (`buildProxy` callback) fires on every `set`/`delete` trap, recording a `MutationRecord` with path, action, oldValue, newValue, and timestamp.
|
|
178
|
+
2. Records are pushed to both a trace log (`mutations`) and an undo stack.
|
|
179
|
+
3. Any new mutation clears the redo stack.
|
|
180
|
+
4. `undo()` pops from the undo stack, pushes to the redo stack, and applies the inverse operation (restoring the previous value, or deleting if it was new).
|
|
181
|
+
5. `redo()` pops from the redo stack, pushes back to the undo stack, and re-applies the original mutation.
|
|
182
|
+
6. During undo/redo, history tracking is temporarily disabled to prevent recursive recording.
|
|
183
|
+
7. `diff()` does a recursive key-by-key comparison between the snapshot and current `deepRaw(state)`.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Best Practices
|
|
188
|
+
|
|
189
|
+
1. **Always snapshot before AI experimentation** — `const snap = memorio.snapshot()` gives you a safe rollback point.
|
|
190
|
+
2. **Call `diff()` before `rollback()`** — inspect what changed first; sometimes you only need to revert one key.
|
|
191
|
+
3. **Keep `maxHistory` reasonable** — the default (100) is fine for most apps. Lower it for memory-constrained environments.
|
|
192
|
+
4. **Don't rely on trace for sensitive data** — the trace log records *all* values written, including tokens/PII. Clear it or disable tracing in production paths.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Introspection & Inspection - Memorio
|
|
2
|
+
|
|
3
|
+
> ✅ **Universal**: Works in Browser, Node.js, Deno, and Edge Workers
|
|
4
|
+
|
|
5
|
+
Introspection utilities let you programmatically discover, verify, and read the shape of the global `state` proxy. Essential for AI agents that need to check whether a path exists before writing, or enumerate available keys before reading.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## stateKeys()
|
|
10
|
+
|
|
11
|
+
Returns all top-level keys currently on the `state` proxy. Excludes internal properties.
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import 'memorio'
|
|
15
|
+
|
|
16
|
+
state.user = { name: 'Sara' }
|
|
17
|
+
state.counter = 42
|
|
18
|
+
|
|
19
|
+
memorio.stateKeys() // ['user', 'counter']
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## pathExists(path)
|
|
25
|
+
|
|
26
|
+
Checks whether a dotted path exists in state. Returns `true` if the path resolves to a non-undefined value.
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
state.user = { name: 'Sara', profile: { age: 30 } }
|
|
30
|
+
|
|
31
|
+
memorio.pathExists('user') // true
|
|
32
|
+
memorio.pathExists('user.name') // true
|
|
33
|
+
memorio.pathExists('user.profile') // true
|
|
34
|
+
memorio.pathExists('user.profile.age') // true
|
|
35
|
+
memorio.pathExists('user.age') // false
|
|
36
|
+
memorio.pathExists('nonexistent') // false
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This is critical for AI agents: always check `pathExists` before writing to a nested path to avoid creating unintended intermediate objects.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## stateType(path)
|
|
44
|
+
|
|
45
|
+
Returns the runtime type of the value at a given state path.
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
state.count = 42
|
|
49
|
+
state.name = 'Sara'
|
|
50
|
+
state.items = [1, 2, 3]
|
|
51
|
+
|
|
52
|
+
memorio.stateType('count') // 'number'
|
|
53
|
+
memorio.stateType('name') // 'string'
|
|
54
|
+
memorio.stateType('items') // 'array'
|
|
55
|
+
memorio.stateType('missing') // 'undefined'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## stateGet(path)
|
|
61
|
+
|
|
62
|
+
Returns the value at a dotted path, deep-cloned to prevent accidental mutation of state.
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
state.user = { name: 'Sara', tags: ['admin'] }
|
|
66
|
+
|
|
67
|
+
const user = memorio.stateGet('user') // { name: 'Sara', tags: ['admin'] }
|
|
68
|
+
user.name = 'Luigi' // mutates the clone, not state
|
|
69
|
+
state.user.name // still 'Sara'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## stateSchema()
|
|
75
|
+
|
|
76
|
+
Generates a full schema report of the current state tree — every path with its type and whether it's defined.
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
state.user = { name: 'Sara', age: 30 }
|
|
80
|
+
state.theme = 'dark'
|
|
81
|
+
|
|
82
|
+
memorio.stateSchema()
|
|
83
|
+
// [
|
|
84
|
+
// { path: 'user', type: 'object', defined: true },
|
|
85
|
+
// { path: 'user.name', type: 'string', defined: true },
|
|
86
|
+
// { path: 'user.age', type: 'number', defined: true },
|
|
87
|
+
// { path: 'theme', type: 'string', defined: true }
|
|
88
|
+
// ]
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This is the **most useful for AI agents** — it gives a complete picture of what's in state and what types the values are, in a single call. Perfect for:
|
|
92
|
+
- Discovering available state before generating code
|
|
93
|
+
- Validating that expected paths exist
|
|
94
|
+
- Understanding the shape of nested objects
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Full API
|
|
99
|
+
|
|
100
|
+
| Method | Parameters | Returns | Description |
|
|
101
|
+
|--------|-----------|---------|-------------|
|
|
102
|
+
| `memorio.stateKeys()` | none | `string[]` | Top-level state keys |
|
|
103
|
+
| `memorio.pathExists(path)` | `string` | `boolean` | Whether a path resolves to a value |
|
|
104
|
+
| `memorio.stateType(path)` | `string` | `string` | Runtime type at path |
|
|
105
|
+
| `memorio.stateGet(path)` | `string` | `any` | Deep-cloned value at path |
|
|
106
|
+
| `memorio.stateSchema()` | none | `SchemaEntry[]` | Full state tree report |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## How It Works
|
|
111
|
+
|
|
112
|
+
All introspection functions read from the global `state` proxy via `deepRaw()` — the same function used internally by the state proxy to unwrap itself before storing. This ensures consistent, non-proxied values are returned.
|
|
113
|
+
|
|
114
|
+
- `pathExists` and `stateGet` split the path on `.` and traverse the state tree.
|
|
115
|
+
- `stateSchema` recursively walks the state object, collecting every path and its type.
|
|
116
|
+
- All returned values from `stateGet` and `snapshot` are deep clones (via JSON round-trip) to prevent accidental mutation.
|