memorio 4.9.35 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -3
- package/README.md +307 -359
- package/SECURITY.md +17 -1
- package/SUMMARY.md +59 -45
- package/adr/001-state-proxy-model.md +96 -0
- package/adr/002-observer-semantics.md +180 -0
- package/adr/003-deep-mutation-semantics.md +129 -0
- package/adr/004-array-mutation-semantics.md +128 -0
- package/adr/005-scheduler-contract.md +149 -0
- package/adr/006-context-isolation.md +92 -0
- package/adr/007-mutation-records.md +118 -0
- package/adr/008-transactions.md +106 -0
- package/adr/009-history-model.md +110 -0
- package/adr/README.md +46 -0
- package/adr/template.md +49 -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 +4594 -0
- package/global.d.ts +8 -0
- package/global.js +4532 -0
- package/index.cjs +700 -678
- package/index.d.ts +1 -0
- package/index.js +680 -677
- package/llms.txt +72 -4
- package/markdown/AUDIT-REPORT.md +135 -0
- package/markdown/CACHE.md +100 -0
- package/markdown/CHANGELOG.md +243 -0
- package/markdown/DEVTOOLS.md +129 -0
- package/markdown/DISPATCH.md +177 -0
- package/markdown/HISTORY.md +199 -0
- package/markdown/IDB.md +178 -0
- package/markdown/IMPORT.md +153 -0
- package/markdown/INSPECT.md +123 -0
- package/markdown/LOGGER.md +154 -0
- package/markdown/MEMORY-ATTACHMENT.md +96 -0
- package/markdown/MEMORY.md +162 -0
- package/markdown/OBSERVER.md +209 -0
- package/markdown/PLATFORM.md +271 -0
- package/markdown/PROJECT.md +311 -0
- package/markdown/SCHEMA.md +176 -0
- package/markdown/SECURITY.md +330 -0
- package/markdown/SESSION.md +165 -0
- package/markdown/SQLITE.md +190 -0
- package/markdown/STATE.md +160 -0
- package/markdown/STORE.md +171 -0
- package/markdown/SYNC.md +319 -0
- package/markdown/TYPED.md +165 -0
- package/markdown/USEOBSERVER.md +257 -0
- package/modules/redux.cjs +320 -167
- package/modules/redux.cjs.map +1 -1
- package/modules/redux.js +320 -167
- package/modules/redux.js.map +1 -1
- package/package.json +13 -3
- package/types/env.d.ts +19 -9
- package/types/exports.d.ts +20 -0
- package/types/history.d.ts +13 -1
- package/types/memorio.d.ts +17 -5
- package/types/mutation.d.ts +75 -0
package/SECURITY.md
CHANGED
|
@@ -34,7 +34,23 @@ Addresses OWASP Top 10 (2021):
|
|
|
34
34
|
- A09:2021 - Security Logging and Monitoring Failures (DevTools inspect, Logger module)
|
|
35
35
|
- A10:2021 - Server-Side Request Forgery (N/A - no network requests)
|
|
36
36
|
|
|
37
|
-
##
|
|
37
|
+
## Global API Security
|
|
38
|
+
|
|
39
|
+
Memorio does **not** expose its APIs on `globalThis` by default. The opt-in
|
|
40
|
+
entrypoint `import 'memorio/global'` installs `state`, `store`, `session`,
|
|
41
|
+
`cache`, `idb`, `sqlite`, `observer`, and `useObserver` on `globalThis`.
|
|
42
|
+
This is an explicit consumer choice - not inferred from `NODE_ENV` or
|
|
43
|
+
bundler environment variables.
|
|
44
|
+
|
|
45
|
+
For application code, prefer explicit named imports:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { state } from 'memorio'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
See [SECURITY-HARDENING.md](./SECURITY-HARDENING.md) for a full hardening
|
|
52
|
+
guide covering storage encryption, context isolation, logger data capture,
|
|
53
|
+
and platform capability assumptions.
|
|
38
54
|
|
|
39
55
|
If you find a security vulnerability:
|
|
40
56
|
|
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 v4.9.5 (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,96 @@
|
|
|
1
|
+
# ADR-001: State Proxy Model
|
|
2
|
+
|
|
3
|
+
> **Status:** Accepted
|
|
4
|
+
> **Date:** 2026-09-12
|
|
5
|
+
> **Deciders:** Memorio 5.x Core Team
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Memorio's reactive state is built on a JavaScript `Proxy`. Every read of
|
|
10
|
+
a nested object re-wraps the child in a Proxy so that deep
|
|
11
|
+
`set`/`delete` traps fire and dispatch events. The design must answer:
|
|
12
|
+
|
|
13
|
+
- How are proxy wrappers cached to avoid exponential allocation on
|
|
14
|
+
repeated reads?
|
|
15
|
+
- What is the reference-identity contract between the proxy and the
|
|
16
|
+
raw target?
|
|
17
|
+
- Which property names are reserved and cannot be used as state keys?
|
|
18
|
+
- How are non-plain objects (Date, Set, Map, functions) handled?
|
|
19
|
+
|
|
20
|
+
## Assumptions
|
|
21
|
+
|
|
22
|
+
- The Proxy target is always a plain `Object` or `Array`.
|
|
23
|
+
- A single `buildProxy` invocation creates one Proxy per raw object.
|
|
24
|
+
- The Proxy is shared across all import paths via `globalThis`
|
|
25
|
+
singletons (see ADR-006).
|
|
26
|
+
- `__DEV__` is a build-time constant (replaced to `false` in
|
|
27
|
+
production, `true` in dev/test).
|
|
28
|
+
|
|
29
|
+
## Decision
|
|
30
|
+
|
|
31
|
+
### Wrapper caching
|
|
32
|
+
|
|
33
|
+
`buildProxy` caches the wrapper in a `WeakMap<rawTarget, proxyWrapper>`.
|
|
34
|
+
On every `get` trap, after unwrapping with `TARGET`, the result is looked
|
|
35
|
+
up in the cache; if present, the existing wrapper is returned instead of
|
|
36
|
+
allocating a new one. This keeps repeated nested reads at O(1) in
|
|
37
|
+
allocation and O(1) in proxy depth.
|
|
38
|
+
|
|
39
|
+
### Reference identity
|
|
40
|
+
|
|
41
|
+
- `TARGET` is a `Symbol` accessor on the proxy that returns the raw
|
|
42
|
+
underlying target. `state[TARGET]` === raw root.
|
|
43
|
+
- `state.a[TARGET]` === raw target's `a` property.
|
|
44
|
+
- `JSON.stringify(proxy)` works because `ownKeys` and
|
|
45
|
+
`getOwnPropertyDescriptor` delegate to `Reflect`.
|
|
46
|
+
- `deepRaw(value)` unwraps all Memorio proxies recursively using
|
|
47
|
+
`TARGET` and a `WeakSet` for cycle safety, returning a plain clone
|
|
48
|
+
with no wrapper references.
|
|
49
|
+
|
|
50
|
+
### Reserved property names
|
|
51
|
+
|
|
52
|
+
`protect` array in `core/internal.ts` reserves these keys on the root
|
|
53
|
+
`state` proxy:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
list, state, store, idb, cache, sqlite, observer, useObserver,
|
|
57
|
+
remove, removeAll, _platform, _capabilities, _sessionId
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Setting any of these keys as state data is rejected by the `set` trap
|
|
61
|
+
with a console error. Attempting to set any `_`-prefixed key that
|
|
62
|
+
collides with internal bookkeeping is also guarded.
|
|
63
|
+
|
|
64
|
+
### Non-plain objects
|
|
65
|
+
|
|
66
|
+
- `Date`, `Set`, `Map`, `RegExp`, functions, and other non-plain
|
|
67
|
+
instances are returned by reference — they are **not** re-wrapped in a
|
|
68
|
+
Proxy.
|
|
69
|
+
- `deepRaw` only traverses `Object` and `Array` constructors; all other
|
|
70
|
+
types are returned by reference.
|
|
71
|
+
|
|
72
|
+
### Path tracking
|
|
73
|
+
|
|
74
|
+
Every `get` trap for a string property sets `internal.lastAccessedPath`
|
|
75
|
+
to the full dotted path (`state.foo.bar`). When
|
|
76
|
+
`internal.tracking` is true, the path is also added to
|
|
77
|
+
`internal.trackedPaths`.
|
|
78
|
+
|
|
79
|
+
## Consequences
|
|
80
|
+
|
|
81
|
+
- **Positive:** O(1) wrapper allocation on repeated nested reads; no
|
|
82
|
+
proxy-depth accumulation on spread updates (regression tested).
|
|
83
|
+
- **Positive:** `TARGET` symbol prevents prototype-pollution attacks
|
|
84
|
+
via `__proto__`/`constructor`/`prototype` because those are string
|
|
85
|
+
keys that are simply treated as data keys on the raw target.
|
|
86
|
+
- **Positive:** `deepRaw` produces clean data for persistence and
|
|
87
|
+
snapshots without leaking proxy wrappers.
|
|
88
|
+
- **Negative:** The `WeakMap` cache is per-module-instance, but since
|
|
89
|
+
the proxy target is a singleton on `globalThis`, the cache is
|
|
90
|
+
effectively shared across import paths.
|
|
91
|
+
- **Negative:** Non-plain objects cannot be deeply observed for inner
|
|
92
|
+
mutations — this is an accepted limitation documented in the API.
|
|
93
|
+
|
|
94
|
+
## Compliance tests
|
|
95
|
+
|
|
96
|
+
- `tests/vitest/tests/contracts/adr-001-state.test.ts`
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# ADR-002: Observer Semantics
|
|
2
|
+
|
|
3
|
+
> **Status:** Proposed
|
|
4
|
+
> **Date:** 2026-09-12
|
|
5
|
+
> **Deciders:** Memorio 5.x Core Team
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Memorio must freeze observer behavior so that developers can reason
|
|
10
|
+
about *when* and *why* their callbacks fire. The observer system is the
|
|
11
|
+
bridge between state mutations and UI updates, between the Mutation
|
|
12
|
+
Engine and the reactive system. Before adding advanced features (impact
|
|
13
|
+
analysis, simulation, causal graph), the observer contract must be
|
|
14
|
+
explicit and deterministic.
|
|
15
|
+
|
|
16
|
+
The following questions must have explicit answers:
|
|
17
|
+
|
|
18
|
+
1. Does a deep mutation notify the leaf observer?
|
|
19
|
+
2. Does a deep mutation notify parent observers?
|
|
20
|
+
3. What happens for array mutations?
|
|
21
|
+
4. What is the reference identity contract for observer callbacks?
|
|
22
|
+
5. Are notifications synchronous or asynchronous?
|
|
23
|
+
6. Are multiple mutations batched into a single notification?
|
|
24
|
+
7. What is the ordering guarantee when multiple observers listen to
|
|
25
|
+
overlapping paths?
|
|
26
|
+
8. Can an observer mutate state synchronously?
|
|
27
|
+
9. What happens when an observer throws?
|
|
28
|
+
10. What happens when an observer is removed during dispatch?
|
|
29
|
+
11. How are nested transactions handled with respect to observer
|
|
30
|
+
notification?
|
|
31
|
+
|
|
32
|
+
## Assumptions
|
|
33
|
+
|
|
34
|
+
- The state Proxy fires exactly one callback per `set` or `delete`
|
|
35
|
+
trap (no double-firing for the same logical mutation).
|
|
36
|
+
- The dispatch layer (`core/dispatch.ts`) is the sole event bus.
|
|
37
|
+
- `useObserver` is a thin wrapper over `observer` + `dispatch.listen`.
|
|
38
|
+
- History tracking is opt-in (`enableHistory(true)`).
|
|
39
|
+
|
|
40
|
+
## Decision
|
|
41
|
+
|
|
42
|
+
### 1. Leaf notification
|
|
43
|
+
|
|
44
|
+
A mutation at path `state.user.profile.name` dispatches an event on
|
|
45
|
+
exactly the path `state.user.profile.name`. Only observers registered
|
|
46
|
+
on that exact path receive the notification. Observers on ancestor
|
|
47
|
+
paths (`state.user.profile`, `state.user`, `state`) do **not** fire for
|
|
48
|
+
a leaf mutation unless the mutation replaces the ancestor itself.
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
observer('state.user.profile.name', cb) // fires
|
|
52
|
+
observer('state.user.profile', cb) // does NOT fire
|
|
53
|
+
observer('state.user', cb) // does NOT fire
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This is the **exact-path observer** model. It is intentional:
|
|
57
|
+
coarse-grained ancestor notification is handled by `useObserver`'s
|
|
58
|
+
auto-discovery mode (which explicitly registers on each accessed path),
|
|
59
|
+
not by event bubbling.
|
|
60
|
+
|
|
61
|
+
### 2. Deep mutation notification
|
|
62
|
+
|
|
63
|
+
When `state.user.profile.name = "Alice"` executes, the set trap fires
|
|
64
|
+
on the `name` property of the `profile` proxy. The callback receives
|
|
65
|
+
`{ path: 'user.profile.name', ... }`. The event dispatched is
|
|
66
|
+
`state.user.profile.name`. Only the leaf observer fires.
|
|
67
|
+
|
|
68
|
+
### 3. Array mutations
|
|
69
|
+
|
|
70
|
+
Array mutations at index `i` dispatch on the **array path**
|
|
71
|
+
(`state.items`), not the index path. This is because array identity
|
|
72
|
+
matters for UI reconciliation (React list rendering).
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
state.items.push(3) // dispatches 'state.items'
|
|
76
|
+
state.items[0] = 99 // dispatches 'state.items'
|
|
77
|
+
state.items.sort() // dispatches 'state.items'
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 4. Reference identity
|
|
81
|
+
|
|
82
|
+
Observer callbacks receive the event object. The callback function
|
|
83
|
+
itself is stored by reference in the dispatch layer. Re-registering the
|
|
84
|
+
same function on the same path replaces the old listener (single-slot
|
|
85
|
+
behavior in `observer`, multi-subscriber in `dispatch.listen`).
|
|
86
|
+
|
|
87
|
+
`useObserver` deduplicates identical path registrations.
|
|
88
|
+
|
|
89
|
+
### 5. Synchronous vs asynchronous
|
|
90
|
+
|
|
91
|
+
- **Event dispatch** (`dispatch.set`) is **synchronous** —
|
|
92
|
+
`globalThis.dispatchEvent` runs listeners inline.
|
|
93
|
+
- **Callback notification** via `dispatch.listen` is **asynchronous**
|
|
94
|
+
— wrapped in `queueMicrotask(cb)` (or `Promise.resolve().then(cb)`
|
|
95
|
+
as fallback). This ensures that by the time the callback fires, the
|
|
96
|
+
state Proxy has already committed the mutation.
|
|
97
|
+
|
|
98
|
+
This means:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
state.counter = 1
|
|
102
|
+
// state.counter === 1 is TRUE here (mutation already committed)
|
|
103
|
+
// observer callback hasn't run yet (next microtask)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 6. Batching
|
|
107
|
+
|
|
108
|
+
There is **no automatic batching** of observer callbacks. Each mutation
|
|
109
|
+
dispatches its own microtask. Multiple synchronous mutations in a
|
|
110
|
+
transaction produce multiple notifications — one per mutation.
|
|
111
|
+
|
|
112
|
+
A future scheduler (ADR-005) may introduce microtask/raf batching for
|
|
113
|
+
UI frameworks, but the core engine does not coalesce notifications.
|
|
114
|
+
|
|
115
|
+
### 7. Ordering guarantee
|
|
116
|
+
|
|
117
|
+
When multiple observers listen to the same path, they fire in
|
|
118
|
+
**registration order** (FIFO). The dispatch layer maintains a list of
|
|
119
|
+
handlers per event name.
|
|
120
|
+
|
|
121
|
+
When multiple mutations occur synchronously (without history), they
|
|
122
|
+
dispatch in mutation order. When history is enabled, the undo/redo
|
|
123
|
+
stack preserves mutation order via array index.
|
|
124
|
+
|
|
125
|
+
### 8. Observer mutating state
|
|
126
|
+
|
|
127
|
+
An observer callback **may** mutate state. Because notifications are
|
|
128
|
+
asynchronous (microtask), the mutation triggers a new dispatch cycle
|
|
129
|
+
with its own microtask. There is no immediate re-entrancy. However,
|
|
130
|
+
deeply recursive state mutations from observers are considered a
|
|
131
|
+
application-level bug and are **not** guarded at the engine level.
|
|
132
|
+
|
|
133
|
+
### 9. Observer throwing
|
|
134
|
+
|
|
135
|
+
If an observer callback throws, the error propagates as an unhandled
|
|
136
|
+
rejection (since the callback runs inside a microtask). The dispatch
|
|
137
|
+
layer does **not** catch or swallow errors. A throwing observer does
|
|
138
|
+
not prevent other observers on the same path from firing (each
|
|
139
|
+
callback is wrapped in its own microtask boundary via
|
|
140
|
+
`Promise.resolve().then`).
|
|
141
|
+
|
|
142
|
+
### 10. Observer removed during dispatch
|
|
143
|
+
|
|
144
|
+
Since dispatch is asynchronous (microtask), calling
|
|
145
|
+
`dispatch.remove(path)` or `observer.remove(path)` during a callback
|
|
146
|
+
removes the listener from subsequent dispatches. The current dispatch
|
|
147
|
+
cycle is unaffected — all handlers registered at dispatch time fire.
|
|
148
|
+
|
|
149
|
+
### 11. Transactions and observer notification
|
|
150
|
+
|
|
151
|
+
During a transaction, each mutation inside the transaction dispatches
|
|
152
|
+
normally. The transaction grouping does not suppress notifications.
|
|
153
|
+
However, `undo()` and `redo()` temporarily disable history recording
|
|
154
|
+
(via `internal.historyEnabled = false`), and the inverse/forward
|
|
155
|
+
operations fire the proxy callback — which will dispatch to observers.
|
|
156
|
+
|
|
157
|
+
A future enhancement may batch observer notifications for all mutations
|
|
158
|
+
within a transaction into a single synthetic notification. This ADR
|
|
159
|
+
does **not** define that behavior yet.
|
|
160
|
+
|
|
161
|
+
## Consequences
|
|
162
|
+
|
|
163
|
+
- **Positive:** Deterministic, FIFO-ordered, asynchronous notification
|
|
164
|
+
gives observers a consistent view of state on every call.
|
|
165
|
+
- **Positive:** Microtask scheduling avoids "state not yet committed"
|
|
166
|
+
bugs that plague synchronous observer models.
|
|
167
|
+
- **Positive:** Exact-path matching means observers fire only when the
|
|
168
|
+
specific watched path changes — no spurious re-renders.
|
|
169
|
+
- **Positive:** No batching in the core engine keeps it simple and
|
|
170
|
+
framework-agnostic. Batching is a scheduler-layer concern.
|
|
171
|
+
- **Negative:** Multiple synchronous mutations produce multiple
|
|
172
|
+
microtask notifications — consumers that need coalescing must
|
|
173
|
+
implement it (or use the future scheduler).
|
|
174
|
+
- **Negative:** Throwing observers produce unhandled rejections rather
|
|
175
|
+
than being caught — this is intentional (fail fast) but may surprise
|
|
176
|
+
consumers expecting error isolation.
|
|
177
|
+
|
|
178
|
+
## Compliance tests
|
|
179
|
+
|
|
180
|
+
- `tests/vitest/tests/contracts/adr-002-observer.test.ts`
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# ADR-003: Deep Mutation Semantics
|
|
2
|
+
|
|
3
|
+
> **Status:** Proposed
|
|
4
|
+
> **Date:** 2026-09-12
|
|
5
|
+
> **Deciders:** Memorio 5.x Core Team
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Deep mutations (e.g. `state.user.profile.name = "Alice"`) are the most
|
|
10
|
+
common source of subtle bugs in proxy-based state libraries. The
|
|
11
|
+
4.7.3 baseline had forensic defects around deep mutation notifications:
|
|
12
|
+
observers not firing, paths not resolved correctly, payloads missing
|
|
13
|
+
arguments. While these defects were reported as not reproducible in
|
|
14
|
+
repo v4.9.10, the semantics remain **undocumented** — they rely on
|
|
15
|
+
accidental Proxy behavior rather than an explicit contract.
|
|
16
|
+
|
|
17
|
+
This ADR makes the deep mutation semantics explicit.
|
|
18
|
+
|
|
19
|
+
## Assumptions
|
|
20
|
+
|
|
21
|
+
- `buildProxy` recursively wraps nested plain objects and arrays.
|
|
22
|
+
- Each Proxy tracks its dotted path via the `tree` array.
|
|
23
|
+
- The set trap computes `path = objPath(key, tree)` — a dotted path
|
|
24
|
+
relative to `state` (e.g. `user.profile.name`).
|
|
25
|
+
- The callback receives `{ action, path, newValue, previousValue }`.
|
|
26
|
+
- The proxy stores **raw** values (via `deepRaw`) on the raw target.
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
### Deep mutation notification scope
|
|
31
|
+
|
|
32
|
+
When `state.user.profile.name = "Alice"` executes:
|
|
33
|
+
|
|
34
|
+
1. The set trap on the deepest proxy (the `profile` object) fires.
|
|
35
|
+
2. `path` is computed as `user.profile.name` (relative to `state`).
|
|
36
|
+
3. The callback receives `{ path: "user.profile.name", ... }`.
|
|
37
|
+
4. `dispatch.set("state.user.profile.name", ...)` is called.
|
|
38
|
+
5. **Only** observers on `state.user.profile.name` fire.
|
|
39
|
+
6. `state.user.profile.name` is updated on the raw target to `"Alice"`.
|
|
40
|
+
|
|
41
|
+
Observers on `state.user.profile`, `state.user`, or `state` do **not**
|
|
42
|
+
receive a notification for this mutation. This is the exact-path
|
|
43
|
+
contract (see ADR-002, §1).
|
|
44
|
+
|
|
45
|
+
### Reference identity after deep mutation
|
|
46
|
+
|
|
47
|
+
After `state.user.profile.name = "Alice"`:
|
|
48
|
+
- `state.user` returns the **same** proxy wrapper (cached via WeakMap).
|
|
49
|
+
- `state.user.profile` returns the **same** proxy wrapper.
|
|
50
|
+
- `state.user.profile.name` returns `"Alice"` (the new primitive).
|
|
51
|
+
- `state.user.profile[TARGET]` returns the same raw object as before
|
|
52
|
+
(the object was mutated in place, not replaced).
|
|
53
|
+
|
|
54
|
+
### Intermediate object creation
|
|
55
|
+
|
|
56
|
+
If a deep path does not exist yet (`state.user.profile` is undefined),
|
|
57
|
+
and a consumer writes `state.user.profile = { name: "Alice" }`:
|
|
58
|
+
1. The set trap on `user` fires with `path = "user.profile"`.
|
|
59
|
+
2. The full object `{ name: "Alice" }` is stored as the raw value.
|
|
60
|
+
3. A new proxy wrapper is created for `{ name: "Alice" }` on the next
|
|
61
|
+
read.
|
|
62
|
+
4. The event dispatched is `state.user.profile` — **not**
|
|
63
|
+
`state.user.profile.name`.
|
|
64
|
+
|
|
65
|
+
There is **no automatic intermediate object creation** during deep
|
|
66
|
+
assignment to nested proxies. If `state.user` is undefined and the
|
|
67
|
+
consumer writes `state.user.profile.name = "Alice"`, this throws a
|
|
68
|
+
TypeError (cannot set property 'profile' of undefined).
|
|
69
|
+
|
|
70
|
+
To set deeply nested values that don't exist yet, use
|
|
71
|
+
`memorio.mutate("state.user.profile.name", "Alice")` which creates
|
|
72
|
+
intermediate objects programmatically.
|
|
73
|
+
|
|
74
|
+
### No-op mutations
|
|
75
|
+
|
|
76
|
+
If a set trap assigns the same value (`state.x = state.x`), the set
|
|
77
|
+
trap still fires. The callback still dispatches the event. However,
|
|
78
|
+
the `mutationToPatch` function returns `null` for `before === after`,
|
|
79
|
+
so no patch is stored in the commit log. The undo/redo stack records
|
|
80
|
+
the mutation (with `before === after`), but undo is a no-op for such
|
|
81
|
+
entries.
|
|
82
|
+
|
|
83
|
+
A future enhancement may add value-comparison in the set trap to skip
|
|
84
|
+
no-op dispatches. This ADR does **not** implement that.
|
|
85
|
+
|
|
86
|
+
### Delete semantics
|
|
87
|
+
|
|
88
|
+
`delete state.user.profile.name`:
|
|
89
|
+
1. The `deleteProperty` trap on the `profile` proxy fires.
|
|
90
|
+
2. `path` is computed as `user.profile.name`.
|
|
91
|
+
3. The callback receives `{ action: "delete", path, previousValue }`.
|
|
92
|
+
4. The event dispatched is `state.user.profile.name`.
|
|
93
|
+
5. The property is removed from the raw target.
|
|
94
|
+
|
|
95
|
+
### Array deep mutation
|
|
96
|
+
|
|
97
|
+
When a nested object inside an array is mutated (`state.items[0].name = "X"`):
|
|
98
|
+
1. The set trap fires on the proxy wrapping `items[0]`.
|
|
99
|
+
2. `path` is computed as `items.0.name` (array index joined by dots).
|
|
100
|
+
3. The event dispatched is `state.items.0.name`.
|
|
101
|
+
4. **Additionally**, because arrays use identity-based reconciliation,
|
|
102
|
+
the observer contract (ADR-002 §3) states that array observers fire
|
|
103
|
+
on the **array path** (`state.items`), not the element path. However,
|
|
104
|
+
the dispatch only fires `state.items.0.name`. Array observers
|
|
105
|
+
registered via `observer('state.items', ...)` will **not** fire for
|
|
106
|
+
`items[0].name` mutations.
|
|
107
|
+
|
|
108
|
+
> **Note:** This is a known limitation. Array element mutations that
|
|
109
|
+
> change object properties do not notify the array-path observer.
|
|
110
|
+
> A future scheduler or mutation engine enhancement may address this
|
|
111
|
+
> by dispatching on both `state.items.0.name` and `state.items`.
|
|
112
|
+
> This ADR documents the current behavior.
|
|
113
|
+
|
|
114
|
+
## Consequences
|
|
115
|
+
|
|
116
|
+
- **Positive:** Deep mutations are deterministic — exactly one event
|
|
117
|
+
per mutation, on the exact leaf path.
|
|
118
|
+
- **Positive:** Reference identity is preserved — the same proxy wrapper
|
|
119
|
+
is reused, preventing proxy-depth accumulation (regression tested).
|
|
120
|
+
- **Positive:** `memorio.mutate()` provides a safe path-based API for
|
|
121
|
+
setting deeply nested values that don't exist yet.
|
|
122
|
+
- **Negative:** Array element object mutations don't notify array-path
|
|
123
|
+
observers — consumers must observe the specific element path.
|
|
124
|
+
- **Negative:** No-op set traps still dispatch events — this is
|
|
125
|
+
intentional for consistency but may cause unnecessary re-renders.
|
|
126
|
+
|
|
127
|
+
## Compliance tests
|
|
128
|
+
|
|
129
|
+
- `tests/vitest/tests/contracts/adr-003-deep-mutation.test.ts`
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# ADR-004: Array Mutation Semantics
|
|
2
|
+
|
|
3
|
+
> **Status:** Proposed
|
|
4
|
+
> **Date:** 2026-09-12
|
|
5
|
+
> **Deciders:** Memorio 5.x Core Team
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Arrays are the most error-prone data structure in proxy-based state
|
|
10
|
+
libraries. The 4.7.3 baseline handled `push` and `pop` but had no
|
|
11
|
+
explicit contract for `sort`, `splice`, `shift`, `unshift`, `fill`,
|
|
12
|
+
`reverse`, `length` changes, or whole-array replacement. Section 9 of
|
|
13
|
+
the Memorio 5.x plan demands explicit, regression-tested behavior for
|
|
14
|
+
every array operation.
|
|
15
|
+
|
|
16
|
+
A mutation that persists but does not notify, or notifies but does not
|
|
17
|
+
persist, is considered a **semantic defect** unless explicitly
|
|
18
|
+
documented.
|
|
19
|
+
|
|
20
|
+
## Assumptions
|
|
21
|
+
|
|
22
|
+
- Arrays are plain JS arrays stored on the Proxy raw target.
|
|
23
|
+
- Array methods (`push`, `pop`, `splice`, etc.) mutate the array in
|
|
24
|
+
place and trigger the set trap on the Proxy wrapping the array.
|
|
25
|
+
- The set trap's `key` for array mutations is the array index or
|
|
26
|
+
`length`.
|
|
27
|
+
- The callback receives `{ action, path, newValue, previousValue }`.
|
|
28
|
+
- The state Proxy dispatches via `dispatch.set("state." + path)`.
|
|
29
|
+
|
|
30
|
+
## Decision
|
|
31
|
+
|
|
32
|
+
### Mutation → Persistence mapping
|
|
33
|
+
|
|
34
|
+
Every array operation below **must** persist to the underlying raw
|
|
35
|
+
array and **must** dispatch an event on the **array path**
|
|
36
|
+
(`state.<arrayPath>`), not on the individual index path. The event
|
|
37
|
+
path is computed from the Proxy `tree` (e.g. for `state.items`, the
|
|
38
|
+
tree is `["items"]`, so the path is `items` and the event is
|
|
39
|
+
`state.items`).
|
|
40
|
+
|
|
41
|
+
| Operation | Persists? | Dispatches on | Patch op |
|
|
42
|
+
|-----------|-----------|---------------|----------|
|
|
43
|
+
| `state.arr[i] = val` | Yes | `state.arr` | `replace` (index i) |
|
|
44
|
+
| `state.arr.push(val)` | Yes | `state.arr` | `add` (at index `arr.length`) |
|
|
45
|
+
| `state.arr.pop()` | Yes | `state.arr` | `remove` (last index) |
|
|
46
|
+
| `state.arr.shift()` | Yes | `state.arr` | `remove` (index 0) |
|
|
47
|
+
| `state.arr.unshift(val)` | Yes | `state.arr` | `add` (at index 0) |
|
|
48
|
+
| `state.arr.splice(i, n, ...items)` | Yes | `state.arr` | `remove` + `add` |
|
|
49
|
+
| `state.arr.sort()` | Yes | `state.arr` | `replace` (whole array) |
|
|
50
|
+
| `state.arr.reverse()` | Yes | `state.arr` | `replace` (whole array) |
|
|
51
|
+
| `state.arr.fill(val, start, end)` | Yes | `state.arr` | `replace` (whole array) |
|
|
52
|
+
| `state.arr.length = n` (truncate) | Yes | `state.arr` | `remove` (removed indices) |
|
|
53
|
+
| `state.arr.length = n` (extend) | Yes | `state.arr` | `add` (new indices) |
|
|
54
|
+
| `state.arr = newArray` | Yes | `state.arr` | `replace` (whole array) |
|
|
55
|
+
| `state.arr[i].prop = val` | Yes | `state.arr.i.prop` | `replace` (leaf) |
|
|
56
|
+
|
|
57
|
+
### sort() persistence
|
|
58
|
+
|
|
59
|
+
`sort()` mutates the array in place and returns a reference to the same
|
|
60
|
+
(sorted) array. The Proxy set trap fires because `length` changes and
|
|
61
|
+
indices are reassigned. The event dispatched is `state.arr`. The
|
|
62
|
+
mutation is **persisted** to the raw target.
|
|
63
|
+
|
|
64
|
+
**Known constraint:** `sort()` with a custom comparator must also
|
|
65
|
+
persist correctly. The comparator function is applied by V8's native
|
|
66
|
+
`Array.prototype.sort`; the Proxy only sees the resulting index
|
|
67
|
+
reassignments.
|
|
68
|
+
|
|
69
|
+
### splice() semantics
|
|
70
|
+
|
|
71
|
+
`splice()` can add, remove, or replace elements at an index. The Proxy
|
|
72
|
+
sees multiple set/delete traps. The event dispatched is `state.arr`
|
|
73
|
+
(the first trap fires, and subsequent traps on the same array path
|
|
74
|
+
are de-duplicated by the dispatch layer's microtask scheduling).
|
|
75
|
+
|
|
76
|
+
### length changes
|
|
77
|
+
|
|
78
|
+
Setting `arr.length = n` where `n < arr.length` truncates the array.
|
|
79
|
+
The deleteProperty trap fires for each removed index. Setting
|
|
80
|
+
`arr.length = n` where `n > arr.length` extends the array with
|
|
81
|
+
`undefined` holes; the set trap fires for `length`.
|
|
82
|
+
|
|
83
|
+
### Whole-array replacement
|
|
84
|
+
|
|
85
|
+
`state.arr = [1, 2, 3]` replaces the entire array. The set trap fires
|
|
86
|
+
once with `path = "arr"`, `newValue = [1, 2, 3]` (deepRaw'd). The
|
|
87
|
+
event dispatched is `state.arr`. Observers on `state.arr` fire.
|
|
88
|
+
Previous array observers (on the old array instance) are orphaned —
|
|
89
|
+
the consumer must re-register if they observe specific indices.
|
|
90
|
+
|
|
91
|
+
### Array element object mutation
|
|
92
|
+
|
|
93
|
+
`state.arr[0].name = "X"` mutates an object inside the array. This
|
|
94
|
+
fires the set trap on the Proxy wrapping `arr[0]` with
|
|
95
|
+
`path = "arr.0.name"`. The event dispatched is `state.arr.0.name`.
|
|
96
|
+
|
|
97
|
+
Per ADR-003, this does **not** notify observers on `state.arr`. This
|
|
98
|
+
is a known limitation. Consumers who need to observe changes to
|
|
99
|
+
objects within arrays must observe the specific element path
|
|
100
|
+
(`state.arr.0.name`) or use `useObserver` auto-discovery.
|
|
101
|
+
|
|
102
|
+
### Event deduplication
|
|
103
|
+
|
|
104
|
+
Because a single array operation (e.g. `splice`) can produce multiple
|
|
105
|
+
set/delete traps, the dispatch layer may fire `state.arr` multiple
|
|
106
|
+
times within the same synchronous block. Each dispatch schedules a
|
|
107
|
+
separate microtask. Observers on `state.arr` will fire **multiple
|
|
108
|
+
times** — once per trap. A future scheduler enhancement may coalesce
|
|
109
|
+
these. This ADR documents the current behavior.
|
|
110
|
+
|
|
111
|
+
## Consequences
|
|
112
|
+
|
|
113
|
+
- **Positive:** Every array operation persists and notifies — no
|
|
114
|
+
silent failures.
|
|
115
|
+
- **Positive:** Array observers fire on the array path, not individual
|
|
116
|
+
indices — this matches React list-reconciliation mental model.
|
|
117
|
+
- **Positive:** `sort()`, `splice()`, and other complex operations are
|
|
118
|
+
explicitly tested for persistence + notification.
|
|
119
|
+
- **Negative:** Multiple traps from a single array operation may
|
|
120
|
+
produce multiple observer notifications — consumers must
|
|
121
|
+
de-duplicate if needed.
|
|
122
|
+
- **Negative:** Array element object mutations do not notify the
|
|
123
|
+
array-path observer — this is a fundamental limitation of the
|
|
124
|
+
exact-path observer model.
|
|
125
|
+
|
|
126
|
+
## Compliance tests
|
|
127
|
+
|
|
128
|
+
- `tests/vitest/tests/contracts/adr-004-arrays.test.ts`
|