memorio 4.9.35 → 5.1.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/AGENTS.md +3 -3
- package/README.md +95 -516
- package/SECURITY.md +159 -33
- package/SUMMARY.md +59 -45
- package/adr/001-state-proxy-model.md +95 -0
- package/adr/002-observer-semantics.md +179 -0
- package/adr/003-deep-mutation-semantics.md +128 -0
- package/adr/004-array-mutation-semantics.md +127 -0
- package/adr/005-scheduler-contract.md +148 -0
- package/adr/006-context-isolation.md +91 -0
- package/adr/007-mutation-records.md +117 -0
- package/adr/008-transactions.md +105 -0
- package/adr/009-history-model.md +109 -0
- package/adr/README.md +46 -0
- package/adr/template.md +48 -0
- package/bin/cli.js +68 -0
- package/examples/basic.ts +115 -115
- package/examples/browser-vanilla.html +358 -358
- package/examples/cache.ts +72 -72
- package/examples/cross-platform-guards.ts +57 -57
- package/examples/history.ts +104 -0
- package/examples/idb.ts +109 -109
- package/examples/multi-tenant-context.ts +44 -44
- package/examples/node-server.ts +308 -308
- package/examples/observer.ts +60 -60
- package/examples/platform.ts +115 -115
- package/examples/react-app.tsx +362 -362
- package/examples/react-observer.tsx +63 -63
- package/examples/semantic-memory.ts +60 -60
- package/examples/session-advanced.ts +91 -91
- package/examples/sqlite-batched-writes.ts +57 -57
- package/examples/state-advanced.ts +89 -89
- package/examples/store-advanced.ts +117 -117
- package/examples/sync.ts +90 -0
- package/examples/typed-and-schema.ts +102 -100
- package/examples/useObserver.tsx +140 -141
- package/global.cjs +5733 -0
- package/global.d.ts +8 -0
- package/global.js +5667 -0
- package/index.cjs +2202 -1041
- package/index.d.ts +2 -0
- package/index.js +2178 -1040
- package/llms.txt +113 -8
- package/markdown/AUDIT-REPORT.md +134 -0
- package/markdown/CACHE.md +191 -0
- package/markdown/DEVTOOLS.md +128 -0
- package/markdown/DISPATCH.md +176 -0
- package/markdown/HISTORY.md +198 -0
- package/markdown/IDB.md +177 -0
- package/markdown/IMPORT.md +152 -0
- package/markdown/INSPECT.md +122 -0
- package/markdown/LOGGER.md +153 -0
- package/markdown/MEMORY-ATTACHMENT.md +95 -0
- package/markdown/MEMORY.md +161 -0
- package/markdown/OBSERVER.md +208 -0
- package/markdown/PLATFORM.md +277 -0
- package/markdown/REDUX.md +54 -0
- package/markdown/SCHEMA.md +175 -0
- package/markdown/SESSION.md +164 -0
- package/markdown/SQLITE.md +189 -0
- package/markdown/STATE.md +159 -0
- package/markdown/STORE.md +170 -0
- package/markdown/SYNC.md +318 -0
- package/markdown/TYPED.md +164 -0
- package/markdown/USEOBSERVER.md +256 -0
- package/modules/redux.cjs +701 -177
- package/modules/redux.cjs.map +1 -1
- package/modules/redux.js +701 -177
- package/modules/redux.js.map +1 -1
- package/package.json +26 -4
- package/types/broadcast.d.ts +61 -0
- package/types/computed.d.ts +96 -0
- package/types/encryption.d.ts +129 -0
- package/types/env.d.ts +19 -9
- package/types/exports.d.ts +29 -0
- package/types/history.d.ts +13 -1
- package/types/memorio.d.ts +25 -6
- package/types/mutation.d.ts +75 -0
- package/types/security.d.ts +67 -0
- package/types/session.d.ts +23 -5
- package/types/store.d.ts +19 -3
- package/vsix/memorio.vsix +0 -0
package/SECURITY.md
CHANGED
|
@@ -1,48 +1,174 @@
|
|
|
1
|
-
# Security
|
|
1
|
+
# Security & Encryption
|
|
2
2
|
|
|
3
|
-
Memorio
|
|
3
|
+
This document covers Memorio's optional encryption layer, client-side multitenancy, and general security posture. For the short version, see the **Security** section in the main [README](./README.md).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Nothing is encrypted by default
|
|
8
|
+
|
|
9
|
+
This applies to every layer: `state`, `store`, `session`, `idb`, `memory`, `sqlite`. Treat browser storage as client-controlled unless you explicitly opt in.
|
|
10
|
+
|
|
11
|
+
Core has zero production dependencies. No `eval`, no dynamic code execution, no bundled telemetry, sanitized keys, validated inputs, bounded journal entries, UUID-based session identifiers. Independently checkable via Socket.dev / Snyk.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The encryption layer
|
|
16
|
+
|
|
17
|
+
Memorio ships an optional encryption layer built on the **Web Crypto API** (`crypto.subtle`): AES-GCM for authenticated encryption, PBKDF2 for password-based key derivation. It is opt-in - no layer encrypts by default, so the common case stays transparent and fast.
|
|
18
|
+
|
|
19
|
+
All `memorio.encryption` operations are **asynchronous by design** - the Web Crypto API runs cryptographic work on a separate internal thread managed by the browser/runtime, so `deriveKey()`, `encrypt()`, and `decrypt()` return Promises that never block the main thread. PBKDF2 derivation at the NIST SP 800-132-recommended 600,000 iterations typically completes in under 300ms on mid-range hardware. No explicit Web Worker offload is needed.
|
|
20
|
+
|
|
21
|
+
### Encrypt a value
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { memorio } from 'memorio'
|
|
25
|
+
|
|
26
|
+
// Derive a key from a password (PBKDF2, 600k iterations - NIST SP 800-132)
|
|
27
|
+
const key = await memorio.encryption.deriveKey('my-password', 'per-tenant-salt')
|
|
28
|
+
// or generate one directly, without a password:
|
|
29
|
+
const freshKey = await memorio.encryption.generateKey()
|
|
30
|
+
const freshSalt = memorio.encryption.generateSalt()
|
|
31
|
+
|
|
32
|
+
// Encrypt → envelope object { __memorio_encrypted, alg, iv, data }
|
|
33
|
+
const envelope = await memorio.encryption.encrypt({ token: 'abc123' }, key)
|
|
34
|
+
|
|
35
|
+
// Decrypt - wrong key rejects rather than returning garbage
|
|
36
|
+
const original = await memorio.encryption.decrypt(envelope, key)
|
|
37
|
+
// → { token: 'abc123' }
|
|
6
38
|
|
|
7
|
-
|
|
8
|
-
|
|
39
|
+
// Check a value before trying to decrypt it
|
|
40
|
+
memorio.encryption.isEncrypted(envelope) // true - checks the __memorio_encrypted flag
|
|
41
|
+
memorio.encryption.isAvailable() // false in environments without Web Crypto (e.g. old runtimes)
|
|
42
|
+
```
|
|
9
43
|
|
|
10
|
-
|
|
44
|
+
For the common "encrypt with a password, get a string back" case, skip manual key derivation:
|
|
11
45
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
46
|
+
```ts
|
|
47
|
+
const cipherText = await memorio.encryption.encryptWithPassword({ token: 'abc123' }, 'my-password')
|
|
48
|
+
const plain = await memorio.encryption.decryptWithPassword(cipherText, 'my-password')
|
|
49
|
+
```
|
|
15
50
|
|
|
16
|
-
|
|
51
|
+
Both accept/return an envelope object (not a JSON string when you already have a key):
|
|
17
52
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- Property-based access control on global objects (`Object.defineProperty` with `enumerable: false`)
|
|
53
|
+
```ts
|
|
54
|
+
const envelope = await memorio.encryption.encrypt({ token: 'abc123' }, key)
|
|
55
|
+
const plain = await memorio.encryption.decrypt(envelope, key) // returns the original value
|
|
56
|
+
```
|
|
23
57
|
|
|
24
|
-
|
|
58
|
+
**Key import/export** for storing CryptoKey material securely (e.g. in an httpOnly cookie):
|
|
25
59
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- A07:2021 - Identification and Authentication Failures (N/A - library, no auth)
|
|
33
|
-
- A08:2021 - Software and Data Integrity Failures (strict tsconfig, lock files)
|
|
34
|
-
- A09:2021 - Security Logging and Monitoring Failures (DevTools inspect, Logger module)
|
|
35
|
-
- A10:2021 - Server-Side Request Forgery (N/A - no network requests)
|
|
60
|
+
```ts
|
|
61
|
+
const key = await memorio.encryption.generateKey()
|
|
62
|
+
const { key: b64, iv } = await memorio.encryption.exportKey(key)
|
|
63
|
+
// Store b64 securely, then restore later:
|
|
64
|
+
const restoredKey = await memorio.encryption.importKey(b64, iv)
|
|
65
|
+
```
|
|
36
66
|
|
|
37
|
-
|
|
67
|
+
### Store with at-rest encryption
|
|
38
68
|
|
|
39
|
-
|
|
69
|
+
```ts
|
|
70
|
+
// Per-call encryption (opt-in)
|
|
71
|
+
await store.set('api_token', secretValue, { encrypt: key })
|
|
72
|
+
const token = await store.get('api_token', { decrypt: key })
|
|
40
73
|
|
|
41
|
-
|
|
42
|
-
|
|
74
|
+
// Or configure a default key for automatic encryption:
|
|
75
|
+
store.config({ encryptionKey: key })
|
|
76
|
+
await store.set('api_token', secretValue) // encrypted transparently
|
|
77
|
+
const token = await store.get('api_token') // decrypted transparently
|
|
78
|
+
```
|
|
43
79
|
|
|
44
|
-
|
|
80
|
+
`session` supports the same `config` and per-call opts.
|
|
81
|
+
|
|
82
|
+
> ⚠️ **Sync vs async changes once encryption is on.** Without encryption, `store.get`/`store.set` (and `session`'s equivalents) are synchronous - they return the value directly. As soon as either call touches `encrypt`/`decrypt` (per-call, or because a default `encryptionKey` is configured), both become asynchronous and return a `Promise`. Code written against the plain synchronous API will silently receive a `Promise` instead of a value if encryption is turned on later - always `await` `store.get`/`store.set` defensively if there's any chance encryption gets enabled down the line.
|
|
45
83
|
|
|
46
84
|
---
|
|
47
|
-
|
|
48
|
-
|
|
85
|
+
|
|
86
|
+
## Contexts are not authorization
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
memorio.createContext('tenant-123')
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Contexts organize and isolate application concerns by namespace-prefixing keys - they are **not** a security boundary. Any script running in the same JS runtime can attempt operations against another context. Real tenant isolation belongs at the auth/backend layer.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Encrypted multitenancy
|
|
97
|
+
|
|
98
|
+
Pass an `encryptionKey` to `createContext()` and the tenant's isolated `store`/`session` automatically encrypt on write and decrypt on read:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const tenantKey = await memorio.encryption.deriveKey(tenantPassword, tenantSalt)
|
|
102
|
+
|
|
103
|
+
const ctx = memorio.createContext('tenant-123', { encryptionKey: tenantKey })
|
|
104
|
+
|
|
105
|
+
// Values are encrypted at rest within this context's namespace
|
|
106
|
+
await ctx.store.set('secrets', { apiKey: '…' })
|
|
107
|
+
const secrets = await ctx.store.get('secrets') // decrypted transparently
|
|
108
|
+
|
|
109
|
+
// Another tenant with a different key can't read this data
|
|
110
|
+
const otherCtx = memorio.createContext('tenant-456', { encryptionKey: otherKey })
|
|
111
|
+
const leaked = await otherCtx.store.get('memorio_store_secrets') // fails to decrypt
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This adds a cryptographic wall on top of the namespace prefix: even if a tenant's key prefix is guessed, the ciphertext is unreadable without the key. It still doesn't replace backend authorization - see above.
|
|
115
|
+
|
|
116
|
+
> ⚠️ **`cache` is never encrypted, even inside an encrypted context.** `createContext(name, { encryptionKey })` only encrypts that context's `store` and `session` - its `cache` stays volatile, in-memory, and in plaintext regardless. Don't put anything in `ctx.cache` you wouldn't put in unencrypted `cache`.
|
|
117
|
+
|
|
118
|
+
`memorio.isolate()` is a thin alias for `createContext()` and accepts the same `{ encryptionKey }` option.
|
|
119
|
+
|
|
120
|
+
> **Key management is yours.** Derive keys from authenticated session material or generate them with `memorio.encryption.generateKey()`. Never hardcode keys in client bundles.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Global API security
|
|
125
|
+
|
|
126
|
+
Memorio does **not** expose its APIs on `globalThis` by default. The opt-in entrypoint `import 'memorio/global'` installs `state`, `store`, `session`, `cache`, `idb`, `sqlite`, `observer`, and `useObserver` on `globalThis` - this is an explicit consumer choice, never inferred from `NODE_ENV` or bundler environment variables.
|
|
127
|
+
|
|
128
|
+
For application code, prefer explicit named imports:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { state } from 'memorio'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Code-level hardening
|
|
137
|
+
|
|
138
|
+
- No hardcoded credentials, API keys, or secrets anywhere in the codebase or build output.
|
|
139
|
+
- Session IDs use a secure random source with graceful fallback: `crypto.randomUUID` → `crypto.getRandomValues` → a documented fallback if neither is available.
|
|
140
|
+
- Public APIs validate their inputs rather than trusting caller-supplied shapes.
|
|
141
|
+
- Global objects installed via `memorio/global` use `Object.defineProperty` with `enumerable: false` where applicable, rather than plain assignment.
|
|
142
|
+
- No obfuscated or minified-beyond-readability code shipped in a way that would hide behavior from review.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Supply chain
|
|
147
|
+
|
|
148
|
+
- Zero production dependencies in core - nothing to audit transitively for the parts you actually ship.
|
|
149
|
+
- Dev dependencies are audited periodically (`npm audit` and equivalent).
|
|
150
|
+
- Package integrity is checkable independently via Socket.dev / Snyk before you adopt a given version.
|
|
151
|
+
|
|
152
|
+
> If you require a specific minimum Socket.dev/Snyk score or a documented audit trail as part of a procurement process, verify the current score yourself at adoption time - this document doesn't pin a number that could go stale.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Reporting a vulnerability
|
|
157
|
+
|
|
158
|
+
Please don't open a public issue for a security vulnerability. Instead:
|
|
159
|
+
|
|
160
|
+
1. Email [Dario Passariello](mailto:dariopassarielloa@gmail.com), or
|
|
161
|
+
2. Use the contact form at https://dario.passariello.ca/contact/
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Enterprise compliance notes
|
|
166
|
+
|
|
167
|
+
| Concern | What you get | Impact |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| **Opt-in encryption** | `memorio.encryption` provides AES-GCM + PBKDF2. `store`/`session` and encrypted contexts support automatic encrypt-on-write/decrypt-on-read. | Not encrypted by default - you must opt in via `store.config({ encryptionKey })` or `createContext(id, { encryptionKey })`. Without it, PII, tokens, and regulated data (GDPR/HIPAA) are at rest in plaintext. Enabling it also changes the calling convention - see the sync/async warning above. |
|
|
170
|
+
| **Client-side multitenancy** | `createContext(id, { encryptionKey })` provides namespace + cryptographic isolation between tenants. | Still **not** a security boundary - any script in the same JS runtime can attempt context operations. Encryption makes cross-tenant reads cryptographically hard, but real authorization must live on the backend. |
|
|
171
|
+
| **DevTools** | Expose data for development only. | Only active when you opt into `memorio/global`; never ship enabled in production. |
|
|
172
|
+
| **Formal compliance frameworks (NIST, OWASP, etc.)** | No formal third-party audit or certification has been published for this project. | If your procurement process requires a certified compliance framework, commission an independent audit rather than relying on self-declared conformance - see [When to use something else](./README.md#when-to-use-something-else) in the README. |
|
|
173
|
+
|
|
174
|
+
If any of these gaps block adoption, keep Memorio for memory/state lifecycle and pair it with a dedicated secrets manager or backend-enforced authorization for the pieces it explicitly doesn't cover.
|
package/SUMMARY.md
CHANGED
|
@@ -1,45 +1,59 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
# Riepilogo dei file - memorio examples & markdown
|
|
2
|
+
|
|
3
|
+
## 📁 examples/ (20 file)
|
|
4
|
+
|
|
5
|
+
| File | Cosa mostra |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `basic.ts` | Tour introduttivo: platform detection, `state`, `store`, `session` in un unico script. |
|
|
8
|
+
| `cache.ts` | Uso base della cache in-memory: set/get, oggetti complessi, cleanup. |
|
|
9
|
+
| `cross-platform-guards.ts` | Come usare `getCapabilities()` invece di `isBrowser()`/`isNode()` per gestire in modo esplicito gli ambienti dove `store`/`session`/`idb` non sono durevoli. |
|
|
10
|
+
| `history.ts` *(nuovo)* | State Intelligence: `snapshot()`/`diff()`/`rollback()` per il pattern "prova → ispeziona → conferma o annulla", undo/redo passo-passo, trace log con export/replay. |
|
|
11
|
+
| `idb.ts` | CRUD completo su IndexedDB: database, tabelle, record, info (size/version/exist). |
|
|
12
|
+
| `multi-tenant-context.ts` | Isolamento dati per richiesta/tenant con `memorio.createContext()` in un handler server-side, incluso cleanup. |
|
|
13
|
+
| `node-server.ts` | Memorio lato server: cache in-memory, fallback di `store`/`session`, contesti multi-tenant, più snippet commentati (Express, WebSocket, job queue, CLI). |
|
|
14
|
+
| `observer.ts` | Pattern observer: singolo valore, oggetti interi, più observer sullo stesso path, cleanup. |
|
|
15
|
+
| `platform.ts` | Platform detection dettagliata + isolamento contesti per server multi-tenant, con verifica esplicita dell'isolamento tra due utenti. |
|
|
16
|
+
| `react-app.tsx` | App React completa (header, profilo, cart, notifiche, settings, login/logout) costruita solo su `state`/`store`/`useObserver`. |
|
|
17
|
+
| `react-observer.tsx` | `useObserver` in due modalità (auto-discovery vs deps espliciti) combinato con `typed<T>()` e `registerSchema()`. |
|
|
18
|
+
| `semantic-memory.ts` | Memoria applicativa per un'app LLM-backed: remember/update con confidence e source, retrieval con `memory.context()` - con nota esplicita che non è ricerca semantica per embedding. |
|
|
19
|
+
| `session-advanced.ts` | Uso avanzato di `session`: auth token, bozza di form, carrello, dimensione dello storage. |
|
|
20
|
+
| `sqlite-batched-writes.ts` | Come evitare di serializzare l'intero DB SQLite ad ogni riga: batch di insert seguito da un solo flush. |
|
|
21
|
+
| `state-advanced.ts` | Stato annidato, array, locking di un valore (`.lock()`), path tracking, rimozione stato. |
|
|
22
|
+
| `store-advanced.ts` | `store` avanzato: persistenza, quota, alias dei metodi, gestione errori, serializzazione di vari tipi. |
|
|
23
|
+
| `sync.ts` *(nuovo)* | Local-first sync: configurazione di un `SyncProvider` (push/pull/resolve), scope `device`/`user`/`shared`, ispezione e replay del journal. |
|
|
24
|
+
| `typed-and-schema.ts` | Tutti gli snippet di `TYPED.md` e `SCHEMA.md` in un unico file TypeScript funzionante: typed state + validazione runtime combinati. |
|
|
25
|
+
| `useObserver.tsx` | Guida step-by-step all'hook `useObserver`: dipendenza singola, multiple, auto-discovery, sync con `useState`, mini to-do app. |
|
|
26
|
+
| `browser-vanilla.html` | Demo HTML/JS pura (nessun bundler) con UI per state, store, session, cache, observer e platform info. |
|
|
27
|
+
|
|
28
|
+
## 📁 markdown/ (24 file)
|
|
29
|
+
|
|
30
|
+
| File | Cosa documenta |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `AUDIT-REPORT.md` | Audit di sicurezza/performance/affidabilità/qualità del codice per la v5.1.0 (datato 2026-09-06), con azioni correttive già applicate. |
|
|
33
|
+
| `CACHE.md` | Reference della cache in-memory: API, quando usarla, limiti. |
|
|
34
|
+
| `CHANGELOG.md` | Storico versioni dalla v2.5.0 alla `Unreleased`, con bugfix, refactoring e note di sicurezza per ogni release. |
|
|
35
|
+
| `DEVTOOLS.md` | Strumenti di debug da console del browser per ispezionare/gestire lo stato di Memorio. |
|
|
36
|
+
| `DISPATCH.md` | Sistema di eventi pub/sub per app vanilla JS (alternativa a `useObserver` fuori da React). |
|
|
37
|
+
| `HISTORY.md` | Reference completa di time-travel: enable/snapshot/diff/undo/redo/rollback/trace, con "how it works" e best practice. |
|
|
38
|
+
| `IDB.md` | Reference IndexedDB: creazione DB/tabelle, CRUD, info sul database. |
|
|
39
|
+
| `IMPORT.md` | I due stili di import (named vs `memorio/global`) e perché condividono la stessa istanza. |
|
|
40
|
+
| `INSPECT.md` | Utility di introspezione per scoprire/verificare la forma dello `state` a runtime - utile per agenti AI. |
|
|
41
|
+
| `LOGGER.md` | Middleware di logging automatico per tracciare le modifiche di stato in console. |
|
|
42
|
+
| `MEMORY-ATTACHMENT.md` | Sistema di "attachment" tra elementi di memoria - estensione opzionale della memoria semantica. |
|
|
43
|
+
| `MEMORY.md` | Reference del layer di memoria semantica: remember/update/context, TTL, confidence, tag, scope. |
|
|
44
|
+
| `OBSERVER.md` | Reference del pattern observer per reagire ai cambi di stato. |
|
|
45
|
+
| `PLATFORM.md` | Platform detection + sistema di context isolation per applicazioni multi-tenant server-side. |
|
|
46
|
+
| `PROJECT.md` | Scheda di progetto interna: versione, team, moduli inclusi, stack tecnologico, target. |
|
|
47
|
+
| `SCHEMA.md` | Validazione runtime dei percorsi di stato: schema oggetto/array/enum/funzione custom. |
|
|
48
|
+
| `SECURITY.md` | Postura di sicurezza del progetto (minacce coperte, cosa NON fa Memorio, come viene gestito l'accesso ai dati). |
|
|
49
|
+
| `SESSION.md` | Reference di `session` (sessionStorage) con fallback in-memory fuori dal browser. |
|
|
50
|
+
| `SQLITE.md` | Reference del layer SQLite via `sql.js`: caricamento lazy, persistenza, query. |
|
|
51
|
+
| `STATE.md` | Reference dello stato reattivo basato su Proxy - il layer centrale di Memorio. |
|
|
52
|
+
| `STORE.md` | Reference di `store` (localStorage) con fallback in-memory fuori dal browser. |
|
|
53
|
+
| `SYNC.md` | Sincronizzazione local-first opzionale: scope, journal, conflict resolution, strategie avanzate per multi-device (HLC, tombstones, fractional indexing). |
|
|
54
|
+
| `TYPED.md` | `memorio.typed<T>()` per la sicurezza dei tipi a compile-time sullo stesso proxy di `state`. |
|
|
55
|
+
| `USEOBSERVER.md` | Reference dell'hook React `useObserver`: modalità auto-discovery vs deps espliciti, tutte le forme di `deps` supportate. |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
**Nota di copertura:** ogni doc in `markdown/` ha ora almeno un esempio corrispondente in `examples/`, **tranne** `DEVTOOLS.md`, `DISPATCH.md`, `INSPECT.md`, `LOGGER.md` e `MEMORY-ATTACHMENT.md` - utile saperlo se in futuro vuoi completare anche quelli.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ADR-001: State Proxy Model
|
|
2
|
+
|
|
3
|
+
> **Status:** Accepted
|
|
4
|
+
> **Date:** 2026-09-12
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Memorio's reactive state is built on a JavaScript `Proxy`. Every read of
|
|
9
|
+
a nested object re-wraps the child in a Proxy so that deep
|
|
10
|
+
`set`/`delete` traps fire and dispatch events. The design must answer:
|
|
11
|
+
|
|
12
|
+
- How are proxy wrappers cached to avoid exponential allocation on
|
|
13
|
+
repeated reads?
|
|
14
|
+
- What is the reference-identity contract between the proxy and the
|
|
15
|
+
raw target?
|
|
16
|
+
- Which property names are reserved and cannot be used as state keys?
|
|
17
|
+
- How are non-plain objects (Date, Set, Map, functions) handled?
|
|
18
|
+
|
|
19
|
+
## Assumptions
|
|
20
|
+
|
|
21
|
+
- The Proxy target is always a plain `Object` or `Array`.
|
|
22
|
+
- A single `buildProxy` invocation creates one Proxy per raw object.
|
|
23
|
+
- The Proxy is shared across all import paths via `globalThis`
|
|
24
|
+
singletons (see ADR-006).
|
|
25
|
+
- `__DEV__` is a build-time constant (replaced to `false` in
|
|
26
|
+
production, `true` in dev/test).
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
### Wrapper caching
|
|
31
|
+
|
|
32
|
+
`buildProxy` caches the wrapper in a `WeakMap<rawTarget, proxyWrapper>`.
|
|
33
|
+
On every `get` trap, after unwrapping with `TARGET`, the result is looked
|
|
34
|
+
up in the cache; if present, the existing wrapper is returned instead of
|
|
35
|
+
allocating a new one. This keeps repeated nested reads at O(1) in
|
|
36
|
+
allocation and O(1) in proxy depth.
|
|
37
|
+
|
|
38
|
+
### Reference identity
|
|
39
|
+
|
|
40
|
+
- `TARGET` is a `Symbol` accessor on the proxy that returns the raw
|
|
41
|
+
underlying target. `state[TARGET]` === raw root.
|
|
42
|
+
- `state.a[TARGET]` === raw target's `a` property.
|
|
43
|
+
- `JSON.stringify(proxy)` works because `ownKeys` and
|
|
44
|
+
`getOwnPropertyDescriptor` delegate to `Reflect`.
|
|
45
|
+
- `deepRaw(value)` unwraps all Memorio proxies recursively using
|
|
46
|
+
`TARGET` and a `WeakSet` for cycle safety, returning a plain clone
|
|
47
|
+
with no wrapper references.
|
|
48
|
+
|
|
49
|
+
### Reserved property names
|
|
50
|
+
|
|
51
|
+
`protect` array in `core/internal.ts` reserves these keys on the root
|
|
52
|
+
`state` proxy:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
list, state, store, idb, cache, sqlite, observer, useObserver,
|
|
56
|
+
remove, removeAll, _platform, _capabilities, _sessionId
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Setting any of these keys as state data is rejected by the `set` trap
|
|
60
|
+
with a console error. Attempting to set any `_`-prefixed key that
|
|
61
|
+
collides with internal bookkeeping is also guarded.
|
|
62
|
+
|
|
63
|
+
### Non-plain objects
|
|
64
|
+
|
|
65
|
+
- `Date`, `Set`, `Map`, `RegExp`, functions, and other non-plain
|
|
66
|
+
instances are returned by reference - they are **not** re-wrapped in a
|
|
67
|
+
Proxy.
|
|
68
|
+
- `deepRaw` only traverses `Object` and `Array` constructors; all other
|
|
69
|
+
types are returned by reference.
|
|
70
|
+
|
|
71
|
+
### Path tracking
|
|
72
|
+
|
|
73
|
+
Every `get` trap for a string property sets `internal.lastAccessedPath`
|
|
74
|
+
to the full dotted path (`state.foo.bar`). When
|
|
75
|
+
`internal.tracking` is true, the path is also added to
|
|
76
|
+
`internal.trackedPaths`.
|
|
77
|
+
|
|
78
|
+
## Consequences
|
|
79
|
+
|
|
80
|
+
- **Positive:** O(1) wrapper allocation on repeated nested reads; no
|
|
81
|
+
proxy-depth accumulation on spread updates (regression tested).
|
|
82
|
+
- **Positive:** `TARGET` symbol prevents prototype-pollution attacks
|
|
83
|
+
via `__proto__`/`constructor`/`prototype` because those are string
|
|
84
|
+
keys that are simply treated as data keys on the raw target.
|
|
85
|
+
- **Positive:** `deepRaw` produces clean data for persistence and
|
|
86
|
+
snapshots without leaking proxy wrappers.
|
|
87
|
+
- **Negative:** The `WeakMap` cache is per-module-instance, but since
|
|
88
|
+
the proxy target is a singleton on `globalThis`, the cache is
|
|
89
|
+
effectively shared across import paths.
|
|
90
|
+
- **Negative:** Non-plain objects cannot be deeply observed for inner
|
|
91
|
+
mutations - this is an accepted limitation documented in the API.
|
|
92
|
+
|
|
93
|
+
## Compliance tests
|
|
94
|
+
|
|
95
|
+
- `tests/vitest/tests/contracts/adr-001-state.test.ts`
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# ADR-002: Observer Semantics
|
|
2
|
+
|
|
3
|
+
> **Status:** Proposed
|
|
4
|
+
> **Date:** 2026-09-12
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Memorio must freeze observer behavior so that developers can reason
|
|
9
|
+
about *when* and *why* their callbacks fire. The observer system is the
|
|
10
|
+
bridge between state mutations and UI updates, between the Mutation
|
|
11
|
+
Engine and the reactive system. Before adding advanced features (impact
|
|
12
|
+
analysis, simulation, causal graph), the observer contract must be
|
|
13
|
+
explicit and deterministic.
|
|
14
|
+
|
|
15
|
+
The following questions must have explicit answers:
|
|
16
|
+
|
|
17
|
+
1. Does a deep mutation notify the leaf observer?
|
|
18
|
+
2. Does a deep mutation notify parent observers?
|
|
19
|
+
3. What happens for array mutations?
|
|
20
|
+
4. What is the reference identity contract for observer callbacks?
|
|
21
|
+
5. Are notifications synchronous or asynchronous?
|
|
22
|
+
6. Are multiple mutations batched into a single notification?
|
|
23
|
+
7. What is the ordering guarantee when multiple observers listen to
|
|
24
|
+
overlapping paths?
|
|
25
|
+
8. Can an observer mutate state synchronously?
|
|
26
|
+
9. What happens when an observer throws?
|
|
27
|
+
10. What happens when an observer is removed during dispatch?
|
|
28
|
+
11. How are nested transactions handled with respect to observer
|
|
29
|
+
notification?
|
|
30
|
+
|
|
31
|
+
## Assumptions
|
|
32
|
+
|
|
33
|
+
- The state Proxy fires exactly one callback per `set` or `delete`
|
|
34
|
+
trap (no double-firing for the same logical mutation).
|
|
35
|
+
- The dispatch layer (`core/dispatch.ts`) is the sole event bus.
|
|
36
|
+
- `useObserver` is a thin wrapper over `observer` + `dispatch.listen`.
|
|
37
|
+
- History tracking is opt-in (`enableHistory(true)`).
|
|
38
|
+
|
|
39
|
+
## Decision
|
|
40
|
+
|
|
41
|
+
### 1. Leaf notification
|
|
42
|
+
|
|
43
|
+
A mutation at path `state.user.profile.name` dispatches an event on
|
|
44
|
+
exactly the path `state.user.profile.name`. Only observers registered
|
|
45
|
+
on that exact path receive the notification. Observers on ancestor
|
|
46
|
+
paths (`state.user.profile`, `state.user`, `state`) do **not** fire for
|
|
47
|
+
a leaf mutation unless the mutation replaces the ancestor itself.
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
observer('state.user.profile.name', cb) // fires
|
|
51
|
+
observer('state.user.profile', cb) // does NOT fire
|
|
52
|
+
observer('state.user', cb) // does NOT fire
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This is the **exact-path observer** model. It is intentional:
|
|
56
|
+
coarse-grained ancestor notification is handled by `useObserver`'s
|
|
57
|
+
auto-discovery mode (which explicitly registers on each accessed path),
|
|
58
|
+
not by event bubbling.
|
|
59
|
+
|
|
60
|
+
### 2. Deep mutation notification
|
|
61
|
+
|
|
62
|
+
When `state.user.profile.name = "Alice"` executes, the set trap fires
|
|
63
|
+
on the `name` property of the `profile` proxy. The callback receives
|
|
64
|
+
`{ path: 'user.profile.name', ... }`. The event dispatched is
|
|
65
|
+
`state.user.profile.name`. Only the leaf observer fires.
|
|
66
|
+
|
|
67
|
+
### 3. Array mutations
|
|
68
|
+
|
|
69
|
+
Array mutations at index `i` dispatch on the **array path**
|
|
70
|
+
(`state.items`), not the index path. This is because array identity
|
|
71
|
+
matters for UI reconciliation (React list rendering).
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
state.items.push(3) // dispatches 'state.items'
|
|
75
|
+
state.items[0] = 99 // dispatches 'state.items'
|
|
76
|
+
state.items.sort() // dispatches 'state.items'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 4. Reference identity
|
|
80
|
+
|
|
81
|
+
Observer callbacks receive the event object. The callback function
|
|
82
|
+
itself is stored by reference in the dispatch layer. Re-registering the
|
|
83
|
+
same function on the same path replaces the old listener (single-slot
|
|
84
|
+
behavior in `observer`, multi-subscriber in `dispatch.listen`).
|
|
85
|
+
|
|
86
|
+
`useObserver` deduplicates identical path registrations.
|
|
87
|
+
|
|
88
|
+
### 5. Synchronous vs asynchronous
|
|
89
|
+
|
|
90
|
+
- **Event dispatch** (`dispatch.set`) is **synchronous** -
|
|
91
|
+
`globalThis.dispatchEvent` runs listeners inline.
|
|
92
|
+
- **Callback notification** via `dispatch.listen` is **asynchronous**
|
|
93
|
+
- wrapped in `queueMicrotask(cb)` (or `Promise.resolve().then(cb)`
|
|
94
|
+
as fallback). This ensures that by the time the callback fires, the
|
|
95
|
+
state Proxy has already committed the mutation.
|
|
96
|
+
|
|
97
|
+
This means:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
state.counter = 1
|
|
101
|
+
// state.counter === 1 is TRUE here (mutation already committed)
|
|
102
|
+
// observer callback hasn't run yet (next microtask)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 6. Batching
|
|
106
|
+
|
|
107
|
+
There is **no automatic batching** of observer callbacks. Each mutation
|
|
108
|
+
dispatches its own microtask. Multiple synchronous mutations in a
|
|
109
|
+
transaction produce multiple notifications - one per mutation.
|
|
110
|
+
|
|
111
|
+
A future scheduler (ADR-005) may introduce microtask/raf batching for
|
|
112
|
+
UI frameworks, but the core engine does not coalesce notifications.
|
|
113
|
+
|
|
114
|
+
### 7. Ordering guarantee
|
|
115
|
+
|
|
116
|
+
When multiple observers listen to the same path, they fire in
|
|
117
|
+
**registration order** (FIFO). The dispatch layer maintains a list of
|
|
118
|
+
handlers per event name.
|
|
119
|
+
|
|
120
|
+
When multiple mutations occur synchronously (without history), they
|
|
121
|
+
dispatch in mutation order. When history is enabled, the undo/redo
|
|
122
|
+
stack preserves mutation order via array index.
|
|
123
|
+
|
|
124
|
+
### 8. Observer mutating state
|
|
125
|
+
|
|
126
|
+
An observer callback **may** mutate state. Because notifications are
|
|
127
|
+
asynchronous (microtask), the mutation triggers a new dispatch cycle
|
|
128
|
+
with its own microtask. There is no immediate re-entrancy. However,
|
|
129
|
+
deeply recursive state mutations from observers are considered a
|
|
130
|
+
application-level bug and are **not** guarded at the engine level.
|
|
131
|
+
|
|
132
|
+
### 9. Observer throwing
|
|
133
|
+
|
|
134
|
+
If an observer callback throws, the error propagates as an unhandled
|
|
135
|
+
rejection (since the callback runs inside a microtask). The dispatch
|
|
136
|
+
layer does **not** catch or swallow errors. A throwing observer does
|
|
137
|
+
not prevent other observers on the same path from firing (each
|
|
138
|
+
callback is wrapped in its own microtask boundary via
|
|
139
|
+
`Promise.resolve().then`).
|
|
140
|
+
|
|
141
|
+
### 10. Observer removed during dispatch
|
|
142
|
+
|
|
143
|
+
Since dispatch is asynchronous (microtask), calling
|
|
144
|
+
`dispatch.remove(path)` or `observer.remove(path)` during a callback
|
|
145
|
+
removes the listener from subsequent dispatches. The current dispatch
|
|
146
|
+
cycle is unaffected - all handlers registered at dispatch time fire.
|
|
147
|
+
|
|
148
|
+
### 11. Transactions and observer notification
|
|
149
|
+
|
|
150
|
+
During a transaction, each mutation inside the transaction dispatches
|
|
151
|
+
normally. The transaction grouping does not suppress notifications.
|
|
152
|
+
However, `undo()` and `redo()` temporarily disable history recording
|
|
153
|
+
(via `internal.historyEnabled = false`), and the inverse/forward
|
|
154
|
+
operations fire the proxy callback - which will dispatch to observers.
|
|
155
|
+
|
|
156
|
+
A future enhancement may batch observer notifications for all mutations
|
|
157
|
+
within a transaction into a single synthetic notification. This ADR
|
|
158
|
+
does **not** define that behavior yet.
|
|
159
|
+
|
|
160
|
+
## Consequences
|
|
161
|
+
|
|
162
|
+
- **Positive:** Deterministic, FIFO-ordered, asynchronous notification
|
|
163
|
+
gives observers a consistent view of state on every call.
|
|
164
|
+
- **Positive:** Microtask scheduling avoids "state not yet committed"
|
|
165
|
+
bugs that plague synchronous observer models.
|
|
166
|
+
- **Positive:** Exact-path matching means observers fire only when the
|
|
167
|
+
specific watched path changes - no spurious re-renders.
|
|
168
|
+
- **Positive:** No batching in the core engine keeps it simple and
|
|
169
|
+
framework-agnostic. Batching is a scheduler-layer concern.
|
|
170
|
+
- **Negative:** Multiple synchronous mutations produce multiple
|
|
171
|
+
microtask notifications - consumers that need coalescing must
|
|
172
|
+
implement it (or use the future scheduler).
|
|
173
|
+
- **Negative:** Throwing observers produce unhandled rejections rather
|
|
174
|
+
than being caught - this is intentional (fail fast) but may surprise
|
|
175
|
+
consumers expecting error isolation.
|
|
176
|
+
|
|
177
|
+
## Compliance tests
|
|
178
|
+
|
|
179
|
+
- `tests/vitest/tests/contracts/adr-002-observer.test.ts`
|