memorio 4.9.30 → 4.9.35
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 +126 -126
- package/README.md +288 -163
- package/examples/cross-platform-guards.ts +57 -57
- package/examples/multi-tenant-context.ts +44 -44
- package/examples/react-observer.tsx +63 -63
- package/examples/semantic-memory.ts +60 -60
- package/examples/sqlite-batched-writes.ts +57 -57
- package/index.cjs +36 -1
- package/index.js +36 -1
- package/llms.txt +1 -1
- package/modules/redux.cjs +274 -240
- package/modules/redux.cjs.map +1 -1
- package/modules/redux.js +274 -240
- package/modules/redux.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,18 +18,30 @@ state.counter++
|
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
No provider tree. No reducers. No actions. No boilerplate.
|
|
21
|
-
Just data
|
|
21
|
+
Just data, available where your application needs it.
|
|
22
|
+
|
|
23
|
+
**And if you don't want a global API, don't use one:**
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { state, store, session, cache, idb, sqlite, memorio } from 'memorio'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The global API is a **choice**, not an architectural requirement.
|
|
22
30
|
|
|
23
31
|
---
|
|
24
32
|
|
|
25
33
|
## Table of contents
|
|
26
34
|
|
|
27
35
|
- [Why memorio](#why-memorio)
|
|
36
|
+
- [Global is optional](#global-is-optional)
|
|
37
|
+
- [Before / After](#before--after)
|
|
28
38
|
- [Which layer should I use](#which-layer-should-i-use)
|
|
29
39
|
- [Install](#install)
|
|
30
40
|
- [Quick start](#quick-start)
|
|
31
41
|
- [Observing changes](#observing-changes)
|
|
32
|
-
- [The layers](#the-layers) —
|
|
42
|
+
- [The layers](#the-layers) — store · session · cache · idb · sqlite · memory
|
|
43
|
+
- [Redux and other state managers](#redux-and-other-state-managers)
|
|
44
|
+
- [memorio vs Redux, at a glance](#memorio-vs-redux-at-a-glance)
|
|
33
45
|
- [React integration](#react-integration)
|
|
34
46
|
- [Typed state & schema validation](#typed-state--schema-validation)
|
|
35
47
|
- [Local-first sync](#local-first-sync)
|
|
@@ -38,62 +50,105 @@ Just data that exists where your application needs it — and grows with it.
|
|
|
38
50
|
- [Honest limitations](#honest-limitations)
|
|
39
51
|
- [When to use something else](#when-to-use-something-else)
|
|
40
52
|
- [Design philosophy](#design-philosophy)
|
|
53
|
+
- [A mental model](#a-mental-model)
|
|
54
|
+
- [Recipes](#recipes)
|
|
41
55
|
- [License](#license)
|
|
42
56
|
|
|
43
57
|
---
|
|
44
58
|
|
|
45
59
|
## Why memorio
|
|
46
60
|
|
|
47
|
-
Modern
|
|
61
|
+
Modern JavaScript applications often split their data across systems that don't talk to each other: a state manager, `localStorage`, `sessionStorage`, IndexedDB, a cache, SQLite, a server database, maybe an AI memory layer, maybe a sync layer on top. Each with its own API, its own lifecycle, its own mental model.
|
|
48
62
|
|
|
49
|
-
memorio
|
|
63
|
+
memorio brings these concerns into **one runtime and one consistent model**, while keeping each layer independent.
|
|
50
64
|
|
|
51
|
-
| Layer | Purpose |
|
|
52
|
-
|
|
53
|
-
| `state` |
|
|
54
|
-
| `cache` | transient runtime data |
|
|
55
|
-
| `session` | session
|
|
56
|
-
| `store` | persistent key/value data |
|
|
57
|
-
| `idb` | durable structured browser data |
|
|
58
|
-
| `sqlite` | relational
|
|
59
|
-
| `memory` |
|
|
60
|
-
| `journal` | local-first operation history |
|
|
65
|
+
| Layer | Purpose | Persistence | Reactive |
|
|
66
|
+
|---|---|---:|---:|
|
|
67
|
+
| `state` | application state | No | Yes |
|
|
68
|
+
| `cache` | transient runtime data | No | No |
|
|
69
|
+
| `session` | tab/session data | Yes | No |
|
|
70
|
+
| `store` | persistent key/value data | Yes | No |
|
|
71
|
+
| `idb` | durable structured browser data | Yes | No |
|
|
72
|
+
| `sqlite` *(beta)* | relational local SQL | Optional | No |
|
|
73
|
+
| `memory` | structured application memory | Yes | Optional |
|
|
74
|
+
| `journal` *(beta)* | local-first operation history | Yes | No |
|
|
75
|
+
|
|
76
|
+
Start with `state`. Add persistence or memory only when your application actually needs it.
|
|
77
|
+
|
|
78
|
+
## Global is optional
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import 'memorio'
|
|
82
|
+
state.user = { name: 'Sara' }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
is useful when an application benefits from a shared runtime and a simple API. But memorio does **not** require applications to expose or use global state — explicit imports work against the exact same runtime:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { state } from 'memorio'
|
|
89
|
+
state.user = { name: 'Sara' }
|
|
90
|
+
```
|
|
61
91
|
|
|
62
|
-
|
|
92
|
+
This makes memorio suitable for small apps, modular apps, libraries, component systems, tests, or applications that already use another state manager and prefer explicit dependencies.
|
|
93
|
+
|
|
94
|
+
**Global when convenient. Explicit when appropriate.**
|
|
63
95
|
|
|
64
96
|
## Which layer should I use
|
|
65
97
|
|
|
66
98
|
```text
|
|
67
99
|
Does the UI need to react automatically to changes?
|
|
68
100
|
│
|
|
69
|
-
├─ Yes → state
|
|
101
|
+
├─ Yes → state
|
|
70
102
|
│
|
|
71
|
-
└─ No
|
|
72
|
-
│
|
|
73
|
-
├─ Survive a reload?
|
|
74
|
-
│ ├─ No → cache
|
|
75
|
-
│ ├─ This tab only → session
|
|
76
|
-
│ └─ Yes, indefinitely→ store (small) or idb (larger/structured)
|
|
103
|
+
└─ No
|
|
77
104
|
│
|
|
78
|
-
├─
|
|
79
|
-
|
|
80
|
-
|
|
105
|
+
├─ Only while the runtime exists? → cache
|
|
106
|
+
├─ Only for this browser tab/session? → session
|
|
107
|
+
├─ Small persistent key/value data? → store
|
|
108
|
+
├─ Larger or structured browser data? → idb
|
|
109
|
+
├─ Relations, joins, or SQL? → sqlite
|
|
110
|
+
└─ Knowledge the application should
|
|
111
|
+
remember (confidence, source, expiry)? → memory
|
|
81
112
|
```
|
|
82
113
|
|
|
83
114
|
## Install
|
|
84
115
|
|
|
85
116
|
```bash
|
|
86
|
-
npm
|
|
117
|
+
npm install memorio
|
|
87
118
|
```
|
|
88
119
|
|
|
89
120
|
Optional peers, only loaded when used:
|
|
90
121
|
|
|
91
122
|
```bash
|
|
92
|
-
npm
|
|
93
|
-
npm
|
|
123
|
+
npm install react react-dom # React integration
|
|
124
|
+
npm install sql.js # sqlite engine
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Zero production dependencies** in the core package. See [Security](#security).
|
|
128
|
+
|
|
129
|
+
## Before / After
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// Without memorio
|
|
133
|
+
const [user, setUser] = useState(null)
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
const saved = localStorage.getItem('user')
|
|
136
|
+
if (saved) setUser(JSON.parse(saved))
|
|
137
|
+
}, [])
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
localStorage.setItem('user', JSON.stringify(user))
|
|
140
|
+
}, [user])
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// With memorio
|
|
145
|
+
import 'memorio'
|
|
146
|
+
store.set('user', { name: 'Sara' }) // persistent immediately, no boilerplate
|
|
147
|
+
|
|
148
|
+
const user = store.get('user')
|
|
94
149
|
```
|
|
95
150
|
|
|
96
|
-
|
|
151
|
+
Same result, one reactive line instead of two effects and manual JSON serialization.
|
|
97
152
|
|
|
98
153
|
## Quick start
|
|
99
154
|
|
|
@@ -101,16 +156,24 @@ npm i sql.js # SQLite engine
|
|
|
101
156
|
import 'memorio'
|
|
102
157
|
|
|
103
158
|
state.user = { name: 'Sara', role: 'admin' }
|
|
104
|
-
|
|
159
|
+
state.counter++
|
|
160
|
+
|
|
161
|
+
console.log(state.user.name)
|
|
162
|
+
console.log(state.counter)
|
|
105
163
|
```
|
|
106
164
|
|
|
107
|
-
Reactive,
|
|
165
|
+
Reactive, Proxy-based state. No provider tree to configure, no reducer to maintain.
|
|
166
|
+
|
|
167
|
+
Lock a slice you don't want mutated by accident:
|
|
108
168
|
|
|
109
169
|
```ts
|
|
110
170
|
state.config = { maxUsers: 100 }
|
|
111
171
|
state.config.lock()
|
|
172
|
+
|
|
112
173
|
state.config.maxUsers = 200 // throws
|
|
174
|
+
|
|
113
175
|
state.config.unlock()
|
|
176
|
+
state.config.maxUsers = 200 // ok
|
|
114
177
|
```
|
|
115
178
|
|
|
116
179
|
Prefer explicit imports over the global? Same runtime either way:
|
|
@@ -121,7 +184,7 @@ import { state, store, session, cache, idb, sqlite, memorio } from 'memorio'
|
|
|
121
184
|
|
|
122
185
|
## Observing changes
|
|
123
186
|
|
|
124
|
-
Reactivity
|
|
187
|
+
Reactivity is part of the runtime — it is not tied to React.
|
|
125
188
|
|
|
126
189
|
```ts
|
|
127
190
|
observer('state.user', (next, previous) => {
|
|
@@ -132,102 +195,40 @@ observer('state.user', (next, previous) => {
|
|
|
132
195
|
observer('state.user.name', callback)
|
|
133
196
|
```
|
|
134
197
|
|
|
135
|
-
|
|
198
|
+
Lower-level event access is also available:
|
|
136
199
|
|
|
137
200
|
```ts
|
|
138
201
|
const off = memorio.dispatch.listen('state.user', event => console.debug(event.detail))
|
|
139
202
|
memorio.dispatch.set('state.user', { detail: { name: 'Sara' } })
|
|
140
203
|
|
|
141
|
-
off()
|
|
142
|
-
memorio.dispatch.remove('state.user') // remove all subscriptions on a name
|
|
204
|
+
off() // remove only this subscription
|
|
143
205
|
```
|
|
144
206
|
|
|
145
|
-
|
|
146
|
-
its own unsubscribe. Prefer the returned `off()` over `remove()` for per-caller
|
|
147
|
-
teardown, since `remove()` clears *every* listener registered under that name.
|
|
148
|
-
|
|
149
|
-
`observer` paths are runtime strings — for compiler-checked access, see [typed state](#typed-state--schema-validation).
|
|
207
|
+
Each subscription owns its own unsubscribe function. Prefer the returned `off()` over `memorio.dispatch.remove('state.user')`, which removes **all** listeners registered under that name.
|
|
150
208
|
|
|
151
209
|
React apps get a dedicated hook, `useObserver` — see [React integration](#react-integration).
|
|
152
210
|
|
|
153
|
-
## Redux / state-manager integration
|
|
154
|
-
|
|
155
|
-
Memorio ships its Redux integration as a **separate, optional entrypoint** (`memorio/redux`). It is
|
|
156
|
-
*not* part of core and is **never imported by `import memorio from 'memorio'`** — so applications
|
|
157
|
-
that don't use Redux pay nothing. Redux is neither a dependency of Memorio nor of the adapter: the
|
|
158
|
-
adapter talks to any store that satisfies a tiny structural shape (`getState` / `subscribe` /
|
|
159
|
-
`dispatch`), so `redux`, Redux Toolkit, Zustand, NgRx, etc. are all accepted without importing the
|
|
160
|
-
`redux` types.
|
|
161
|
-
|
|
162
|
-
> **Ownership rule:** *Redux owns application state. Memorio owns memory.* The adapter only writes
|
|
163
|
-
> **selected** values (declared opt-in mappings) into Memorio memory, and only recalls them once,
|
|
164
|
-
> at bootstrap. It never mirrors the whole Redux tree and never two-way syncs.
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
import memorio from 'memorio'
|
|
168
|
-
import { createMemorioReduxBridge } from 'memorio/redux'
|
|
169
|
-
|
|
170
|
-
type AppState = { user: { name: string; preferences?: { theme?: string } } }
|
|
171
|
-
|
|
172
|
-
const memorioRedux = createMemorioReduxBridge<AppState>({
|
|
173
|
-
// Opt-in: only these paths become durable memory.
|
|
174
|
-
mappings: {
|
|
175
|
-
'user.preferences': {
|
|
176
|
-
selector: (s) => s.user.preferences,
|
|
177
|
-
type: 'preference',
|
|
178
|
-
scope: 'local', // never 'hot' for Redux mappings
|
|
179
|
-
tags: ['app', 'user']
|
|
180
|
-
},
|
|
181
|
-
'user.name': (s) => s.user.name // a bare selector is also accepted
|
|
182
|
-
},
|
|
183
|
-
// Performance: only consider the action types that can mutate mapped state.
|
|
184
|
-
whitelist: ['user/preferencesChanged', 'user/nameChanged'],
|
|
185
|
-
// Diagnostics
|
|
186
|
-
debug: true
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const store = createStore(reducer, applyMiddleware(memorioRedux.middleware))
|
|
190
|
-
|
|
191
|
-
// Bootstrap hydration (Memorio -> Redux, one-shot). The bridge arms a
|
|
192
|
-
// re-entrancy guard so the restore dispatch does not echo back to Memorio.
|
|
193
|
-
await memorioRedux.hydrate(store, {
|
|
194
|
-
onHydration: (values) => store.dispatch(restorePreferences(values))
|
|
195
|
-
})
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Directional flow, no loops:
|
|
199
|
-
|
|
200
|
-
| Direction | Mechanism | Ownership |
|
|
201
|
-
| --- | --- | --- |
|
|
202
|
-
| Redux -> Memorio | middleware persists only `mappings` whose selector changed **and** whose action type isn't blacklisted (#30) | Memorio is the durable owner (#31) |
|
|
203
|
-
| Memorio -> Redux | `hydrate()` recalls once at bootstrap; the app dispatches its own restore actions | Redux owns runtime state |
|
|
204
|
-
|
|
205
|
-
- **No store mirroring:** only mapped selectors are persisted — never `store.getState()` wholesale.
|
|
206
|
-
- **No synchronization loops:** hydration arms `setHydrating(true)` + seeds the change-detection cache, so the restore dispatch is seen as "no change" and is not re-persisted.
|
|
207
|
-
- **Async is off the reducer path:** middleware defers `memorio.memory.remember` to a microtask and coerces a burst of actions into a single flush.
|
|
208
|
-
- **Failure-isolated:** Memorio errors are swallowed by default (`failSoft: true`, default) and routed to `onError`; Redux dispatch always completes normally.
|
|
209
|
-
- **Production-safe:** nothing in the adapter exposes Memorio's database on `globalThis` / DevTools.
|
|
210
|
-
|
|
211
211
|
## The layers
|
|
212
212
|
|
|
213
|
-
### `store` — persistent key/value
|
|
213
|
+
### `store` — persistent key/value data
|
|
214
214
|
|
|
215
215
|
```ts
|
|
216
216
|
store.set('preferences', { theme: 'dark' })
|
|
217
217
|
const preferences = store.get('preferences')
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
Backed by `localStorage` in the browser
|
|
220
|
+
Backed by `localStorage` in the browser. Falls back to memory in environments without persistent storage — check `store.isPersistent` when portability matters.
|
|
221
221
|
|
|
222
|
-
### `session` —
|
|
222
|
+
### `session` — data for the current session
|
|
223
223
|
|
|
224
224
|
```ts
|
|
225
|
-
session.set('
|
|
225
|
+
session.set('wizard-step', 3)
|
|
226
|
+
const step = session.get('wizard-step')
|
|
226
227
|
```
|
|
227
228
|
|
|
228
|
-
Backed by `sessionStorage`. Good for
|
|
229
|
+
Backed by `sessionStorage`. Good for multi-step workflows, temporary preferences, tab-scoped data.
|
|
229
230
|
|
|
230
|
-
### `cache` — volatile
|
|
231
|
+
### `cache` — volatile runtime data
|
|
231
232
|
|
|
232
233
|
```ts
|
|
233
234
|
cache.set('expensive-result', computeExpensiveResult())
|
|
@@ -235,18 +236,19 @@ cache.set('expensive-result', computeExpensiveResult())
|
|
|
235
236
|
|
|
236
237
|
Disappears when the runtime disappears. No persistence guarantee, ever.
|
|
237
238
|
|
|
238
|
-
### `idb` — durable
|
|
239
|
+
### `idb` — durable structured browser data
|
|
239
240
|
|
|
240
241
|
```ts
|
|
241
242
|
await idb.db.create('app')
|
|
242
243
|
await idb.table.create('app', 'users')
|
|
243
244
|
await idb.data.set('app', 'users', { id: 1, name: 'Sara' })
|
|
245
|
+
|
|
244
246
|
const user = await idb.data.get('app', 'users', 1)
|
|
245
247
|
```
|
|
246
248
|
|
|
247
|
-
Check before
|
|
249
|
+
Check runtime support before depending on it: `memorio.getCapabilities()`.
|
|
248
250
|
|
|
249
|
-
### `sqlite` —
|
|
251
|
+
### `sqlite` — local SQL
|
|
250
252
|
|
|
251
253
|
```ts
|
|
252
254
|
await sqlite.ready
|
|
@@ -260,17 +262,17 @@ await sqlite.query.run('app', `INSERT INTO users (name, role) VALUES (?, ?)`, ['
|
|
|
260
262
|
const admins = await sqlite.query.select('app', `SELECT * FROM users WHERE role = ?`, ['admin'])
|
|
261
263
|
```
|
|
262
264
|
|
|
263
|
-
Runs **in memory by default**,
|
|
265
|
+
Runs **in memory by default**, using `sql.js`. Persistence is explicit:
|
|
264
266
|
|
|
265
267
|
```ts
|
|
266
268
|
await sqlite.db.create('app', { persistence: true })
|
|
267
269
|
```
|
|
268
270
|
|
|
269
|
-
> ⚠️ Persistence serializes the **entire** database on
|
|
271
|
+
> ⚠️ **Persistence is not incremental.** It serializes the **entire** database on flush — fine for small/medium local datasets, but large databases should not be persisted after every mutation. Batch writes and flush deliberately. memorio's SQLite layer is an embedded local SQL runtime, not a replacement for a server database.
|
|
270
272
|
|
|
271
|
-
### `memory` —
|
|
273
|
+
### `memory` — application memory
|
|
272
274
|
|
|
273
|
-
The layer that makes memorio more than a state manager.
|
|
275
|
+
The layer that makes memorio more than a state manager. `memory` represents information the application wants to **remember**, not just store — it can carry type, confidence, source, scope, tags, lifetime, status, and history.
|
|
274
276
|
|
|
275
277
|
```ts
|
|
276
278
|
await memorio.memory.remember('user.language', 'Italian', {
|
|
@@ -284,14 +286,14 @@ await memorio.memory.remember('user.language', 'Italian', {
|
|
|
284
286
|
const language = await memorio.memory.recall('user.language')
|
|
285
287
|
```
|
|
286
288
|
|
|
287
|
-
Updates don't overwrite — they
|
|
289
|
+
Updates don't overwrite — they supersede, preserving history:
|
|
288
290
|
|
|
289
291
|
```ts
|
|
290
292
|
await memorio.memory.update('user.language', 'English', { confidence: 0.95 })
|
|
291
|
-
//
|
|
293
|
+
// previous entry → status: 'superseded' | new entry → status: 'active'
|
|
292
294
|
```
|
|
293
295
|
|
|
294
|
-
Retrieve what's
|
|
296
|
+
Retrieve what's relevant to the current operation, not everything:
|
|
295
297
|
|
|
296
298
|
```ts
|
|
297
299
|
const context = await memorio.memory.context({
|
|
@@ -302,13 +304,86 @@ const context = await memorio.memory.context({
|
|
|
302
304
|
})
|
|
303
305
|
```
|
|
304
306
|
|
|
305
|
-
|
|
307
|
+
The context system considers tags, type, confidence, and recency.
|
|
306
308
|
|
|
307
|
-
|
|
309
|
+
> ℹ️ **"Semantic" here means structured, not embedding-based.** `memory.context()` ranks by tags, type, confidence and recency — there is no hidden vector database and no claim that free text has been understood through embeddings. If you need true similarity search, pair a dedicated embedding/vector store with `memorio.memory` for the lifecycle on top (confidence, source, TTL, scope, history, supersession).
|
|
310
|
+
|
|
311
|
+
In short: `sqlite` answers "what data do I have?" — `memory` answers "what does my app remember, and how sure is it?" They're not competing for the same job.
|
|
312
|
+
|
|
313
|
+
## Redux and other state managers
|
|
314
|
+
|
|
315
|
+
memorio does not need to replace your existing state manager. Its Redux integration ships as a **separate, optional entry point**:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
import { createMemorioReduxBridge } from 'memorio/redux'
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Redux is not a dependency of memorio — the adapter works against a small structural interface (`getState` / `subscribe` / `dispatch`), so it can work with Redux-style stores without coupling memorio's core to Redux.
|
|
322
|
+
|
|
323
|
+
> **Ownership rule:** your state manager owns application state. memorio owns memory.
|
|
324
|
+
|
|
325
|
+
The bridge does not mirror the entire store — only explicitly mapped values are persisted:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
const memorioRedux = createMemorioReduxBridge<AppState>({
|
|
329
|
+
mappings: {
|
|
330
|
+
'user.preferences': {
|
|
331
|
+
selector: state => state.user.preferences,
|
|
332
|
+
type: 'preference',
|
|
333
|
+
scope: 'local',
|
|
334
|
+
tags: ['app', 'user'],
|
|
335
|
+
},
|
|
336
|
+
'user.name': state => state.user.name,
|
|
337
|
+
},
|
|
338
|
+
whitelist: ['user/preferencesChanged', 'user/nameChanged'],
|
|
339
|
+
debug: true,
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
const store = createStore(reducer, applyMiddleware(memorioRedux.middleware))
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Hydration is explicit and one-shot:
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
await memorioRedux.hydrate(store, {
|
|
349
|
+
onHydration(values) {
|
|
350
|
+
store.dispatch(restorePreferences(values))
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Direction of ownership — there is intentionally no permanent two-way mirror:
|
|
356
|
+
|
|
357
|
+
```text
|
|
358
|
+
Redux ─────────────→ memorio memorio ────────────→ Redux
|
|
359
|
+
application state durable memory bootstrap hydration
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
- only mapped selectors are persisted — the entire Redux tree is never mirrored
|
|
363
|
+
- hydration is one-shot; restore dispatches do not create synchronization loops
|
|
364
|
+
- persistence happens outside the reducer path; bursts of actions are coalesced
|
|
365
|
+
- memorio failures do not break Redux dispatch — errors can be reported through `onError`
|
|
366
|
+
- the adapter does not expose the memorio database through `globalThis` or DevTools
|
|
367
|
+
|
|
368
|
+
### memorio vs Redux, at a glance
|
|
369
|
+
|
|
370
|
+
Not a replacement — a different scope. Useful mainly for deciding which one (or both) fits a given piece of state.
|
|
371
|
+
|
|
372
|
+
| | Redux | memorio |
|
|
373
|
+
|---|---|---|
|
|
374
|
+
| Core model | single store, plain object | multiple independent layers (`state`, `store`, `session`, `cache`, `idb`, `sqlite`, `memory`) |
|
|
375
|
+
| State changes | via dispatched actions + reducers | direct mutation on a reactive proxy |
|
|
376
|
+
| Persistence | not built in (needs `redux-persist` or similar) | built into `store`/`session`/`idb`/`sqlite` |
|
|
377
|
+
| Structured "memory" (confidence, TTL, source, supersession) | not a concept in Redux | native, via the `memory` layer |
|
|
378
|
+
| Offline sync / conflict resolution | not built in | built into `memory.configure()` (operations + journal + HLC ordering) |
|
|
379
|
+
| Time-travel debugging, strict middleware pipelines | yes, mature ecosystem | not a goal — see [When to use something else](#when-to-use-something-else) |
|
|
380
|
+
| Dependencies | small core, large plugin ecosystem | zero production dependencies |
|
|
381
|
+
|
|
382
|
+
Pick Redux when you need its action-pipeline discipline and tooling. Pick memorio when you want state, persistence, and structured memory under one runtime without wiring several libraries together. Nothing stops you from using both — that's exactly what the [Redux bridge](#redux-and-other-state-managers) is for.
|
|
308
383
|
|
|
309
384
|
## React integration
|
|
310
385
|
|
|
311
|
-
React is an integration, not a requirement — `useObserver`
|
|
386
|
+
React is an integration, not a requirement — `useObserver` connects to the same observer system described above.
|
|
312
387
|
|
|
313
388
|
```tsx
|
|
314
389
|
function Counter() {
|
|
@@ -318,9 +393,11 @@ function Counter() {
|
|
|
318
393
|
}
|
|
319
394
|
```
|
|
320
395
|
|
|
396
|
+
The runtime itself remains independent of React.
|
|
397
|
+
|
|
321
398
|
## Typed state & schema validation
|
|
322
399
|
|
|
323
|
-
TypeScript types
|
|
400
|
+
TypeScript types give compile-time guarantees without creating another state container:
|
|
324
401
|
|
|
325
402
|
```ts
|
|
326
403
|
interface AppState {
|
|
@@ -330,12 +407,12 @@ interface AppState {
|
|
|
330
407
|
|
|
331
408
|
const app = memorio.typed<AppState>()
|
|
332
409
|
app.theme = 'dark'
|
|
333
|
-
app.theme = 'purple' //
|
|
410
|
+
app.theme = 'purple' // TypeScript error
|
|
334
411
|
```
|
|
335
412
|
|
|
336
|
-
`app === state` — same
|
|
413
|
+
`app === state` — same proxy, no duplicated store.
|
|
337
414
|
|
|
338
|
-
Runtime validation
|
|
415
|
+
Runtime validation protects values crossing trust boundaries:
|
|
339
416
|
|
|
340
417
|
```ts
|
|
341
418
|
memorio.registerSchema('user', {
|
|
@@ -347,41 +424,40 @@ memorio.registerSchema('user', {
|
|
|
347
424
|
},
|
|
348
425
|
})
|
|
349
426
|
|
|
350
|
-
state.user = { name: 'Sara' } // rejected
|
|
427
|
+
state.user = { name: 'Sara' } // rejected: missing required "email"
|
|
351
428
|
```
|
|
352
429
|
|
|
353
|
-
|
|
430
|
+
Arrays validate each element via `items`:
|
|
354
431
|
|
|
355
432
|
```ts
|
|
356
|
-
memorio.registerSchema('tags', {
|
|
357
|
-
type: 'array',
|
|
358
|
-
items: { type: 'string' },
|
|
359
|
-
})
|
|
433
|
+
memorio.registerSchema('tags', { type: 'array', items: { type: 'string' } })
|
|
360
434
|
|
|
361
435
|
state.tags = ['a', 'b'] // accepted
|
|
362
|
-
state.tags = [1, 2, 3] // rejected
|
|
436
|
+
state.tags = [1, 2, 3] // rejected
|
|
363
437
|
```
|
|
364
438
|
|
|
365
439
|
## Local-first sync
|
|
366
440
|
|
|
367
|
-
The local application owns its data
|
|
368
|
-
|
|
369
|
-
memorio syncs **operations** (`remember`, `update`, `forget`, `expire`, `confirm`, `supersede`), not database dumps, via a local journal that survives network failure:
|
|
441
|
+
The local application owns its data — the cloud is optional transport. memorio synchronizes **operations** (`remember`, `update`, `forget`, `expire`, `confirm`, `supersede`), not database dumps. A local journal records operations so temporary network failures don't stop the application from working.
|
|
370
442
|
|
|
371
443
|
```ts
|
|
372
444
|
memorio.memory.configure({
|
|
373
445
|
namespace: 'user:123:device:abc',
|
|
374
446
|
provider: {
|
|
375
|
-
push:
|
|
376
|
-
pull:
|
|
447
|
+
push: ops => fetch('/api/sync', { method: 'POST', body: JSON.stringify(ops) }),
|
|
448
|
+
pull: since => fetch(`/api/sync?since=${since}`).then(r => r.json()),
|
|
377
449
|
},
|
|
378
450
|
auto: true,
|
|
379
451
|
})
|
|
380
452
|
```
|
|
381
453
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
454
|
+
```text
|
|
455
|
+
local application → memorio memory → local journal → offline / cloud
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
The network is an extension of the local application, not its prerequisite.
|
|
459
|
+
|
|
460
|
+
Remote operations are ordered causally using HLC timestamps. For concurrent writes to the same key, memorio provides a deterministic default — a custom resolver can override it:
|
|
385
461
|
|
|
386
462
|
```ts
|
|
387
463
|
memorio.memory.configure({
|
|
@@ -392,66 +468,115 @@ memorio.memory.configure({
|
|
|
392
468
|
})
|
|
393
469
|
```
|
|
394
470
|
|
|
395
|
-
|
|
396
|
-
tie. The resolver only settles *client-side* divergence between what memorio has seen locally —
|
|
397
|
-
the provider is still responsible for the final server-side policy.
|
|
471
|
+
Without a resolver, the default policy applies the remote entry on a tie. The resolver operates client-side — the sync provider is still responsible for any final server-side conflict policy.
|
|
398
472
|
|
|
399
473
|
## Cross-platform support
|
|
400
474
|
|
|
401
475
|
| API | Browser | Node.js | Deno | Edge / Workers |
|
|
402
|
-
|
|
476
|
+
|---|:---:|:---:|:---:|:---:|
|
|
403
477
|
| `state` / `observer` / `cache` | ✅ | ✅ | ✅ | ✅ |
|
|
404
478
|
| `store` / `session` | persistent | memory fallback | memory fallback | capability-dependent |
|
|
405
479
|
| `idb` | ✅ | ❌ | ❌ | capability-dependent |
|
|
406
480
|
| `sqlite` | ✅ | ❌ | ❌ | capability-dependent |
|
|
407
|
-
| `devtools` |
|
|
481
|
+
| `devtools` | dev-only | ❌ | ❌ | capability-dependent |
|
|
408
482
|
|
|
409
483
|
```ts
|
|
410
484
|
memorio.getCapabilities()
|
|
411
|
-
memorio.isBrowser() / isNode() / isDeno() / isEdge()
|
|
485
|
+
memorio.isBrowser() / memorio.isNode() / memorio.isDeno() / memorio.isEdge()
|
|
412
486
|
```
|
|
413
487
|
|
|
488
|
+
Do not assume every persistence backend exists in every runtime.
|
|
489
|
+
|
|
414
490
|
## Security
|
|
415
491
|
|
|
416
|
-
- Zero production dependencies — [
|
|
492
|
+
- Zero production dependencies — [checked by Socket.dev](https://socket.dev/npm/package/memorio) and [scanned by Snyk](https://snyk.io/test/npm/memorio)
|
|
417
493
|
- No `eval`, no dynamic code execution, no bundled telemetry
|
|
418
494
|
- Sanitized keys, validated inputs, caught module-boundary errors
|
|
419
|
-
- UUID-based session identifiers
|
|
495
|
+
- Bounded journal entries, UUID-based session identifiers
|
|
420
496
|
|
|
421
|
-
**
|
|
497
|
+
> ⚠️ **memorio does not encrypt data by default.** This applies to `state`, `store`, `session`, `idb`, `memory`, and `sqlite`. Treat browser storage as client-controlled data — if your application handles auth secrets, credentials, or regulated data, bring your own encryption, backend authorization, and server-side security controls.
|
|
422
498
|
|
|
423
|
-
**Contexts
|
|
499
|
+
**Contexts are not security boundaries.** `memorio.createContext('tenant-123')` is useful for organizing and isolating application concerns — it is **not** an authorization mechanism. Code running inside the same JS runtime can potentially access other memorio contexts. Real tenant isolation belongs at the backend/authentication layer.
|
|
500
|
+
|
|
501
|
+
memorio's DevTools are intended for development only, disabled via runtime detection (`process.env.NODE_ENV`) — not build-time removal. Make sure your bundler actually defines this correctly in production; don't treat DevTools absence as a security boundary.
|
|
424
502
|
|
|
425
503
|
## Honest limitations
|
|
426
504
|
|
|
427
|
-
|
|
505
|
+
memorio deliberately documents its edges.
|
|
428
506
|
|
|
429
|
-
-
|
|
430
|
-
- **SQLite persistence is
|
|
431
|
-
- **
|
|
432
|
-
- **
|
|
433
|
-
- **DevTools are
|
|
507
|
+
- **Semantic memory is structured, not vector-based.** `memory.context()` uses structured ranking, not embedding similarity search.
|
|
508
|
+
- **SQLite persistence is not incremental** — the current strategy serializes the complete database on flush; large datasets need deliberate batching.
|
|
509
|
+
- **Contexts and namespaces are not authorization** — they organize application data, they don't replace authentication or backend authorization.
|
|
510
|
+
- **Data is not encrypted automatically** — memorio doesn't pretend local persistence is secure storage.
|
|
511
|
+
- **DevTools are runtime-controlled, not build-removed** — production builds should still configure `NODE_ENV` correctly.
|
|
434
512
|
|
|
435
513
|
## When to use something else
|
|
436
514
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
- **Need
|
|
440
|
-
- **Need
|
|
515
|
+
memorio is intentionally not everything.
|
|
516
|
+
|
|
517
|
+
- **Need strict Redux-style architecture** (action pipelines, reducer-based transitions, extensive middleware, time-travel debugging, event-sourcing)? memorio can integrate with such systems rather than replace them.
|
|
518
|
+
- **Need a server database** (authoritative persistence, multi-user authorization, server-side transactions, backend-controlled access)? Use one — memorio doesn't provide it.
|
|
519
|
+
- **Need true embedding similarity / vector search**? Pair a dedicated vector database with `memorio.memory`, which can still manage the lifecycle and metadata of the retrieved knowledge.
|
|
441
520
|
|
|
442
521
|
## Design philosophy
|
|
443
522
|
|
|
444
|
-
1. **Local first** — the
|
|
445
|
-
2. **Persistence is
|
|
446
|
-
3. **The cloud is optional** —
|
|
447
|
-
4. **
|
|
448
|
-
5. **Memory has meaning** — confidence, source,
|
|
449
|
-
6. **
|
|
450
|
-
7. **
|
|
523
|
+
1. **Local first** — the application stays useful when the network disappears.
|
|
524
|
+
2. **Persistence is optional** — start in memory, persist only when it provides value.
|
|
525
|
+
3. **The cloud is optional** — remote sync extends the local application; it does not define it.
|
|
526
|
+
4. **Use the right primitive** — don't put relational data in a key/value store, or semantic memory in ordinary state.
|
|
527
|
+
5. **Memory has meaning** — confidence, source, type, scope, TTL, tags, history, status. That's different from simply storing a value.
|
|
528
|
+
6. **Global access is a convenience, not a requirement** — both `import 'memorio'` and `import { state } from 'memorio'` are valid; memorio does not impose one architecture.
|
|
529
|
+
7. **Tell the truth about boundaries** — local data is not automatically secure, persistence is not authorization, structured memory is not automatically AI semantic search.
|
|
530
|
+
8. **Keep the common case tiny:**
|
|
451
531
|
```ts
|
|
452
532
|
import 'memorio'
|
|
453
533
|
state.value = 42
|
|
454
534
|
```
|
|
535
|
+
Everything else is there when you need it.
|
|
536
|
+
|
|
537
|
+
## A mental model
|
|
538
|
+
|
|
539
|
+
```text
|
|
540
|
+
memorio
|
|
541
|
+
│
|
|
542
|
+
┌──────────────┼──────────────┐
|
|
543
|
+
│ │ │
|
|
544
|
+
runtime persistence memory
|
|
545
|
+
│ │ │
|
|
546
|
+
┌─────┼─────┐ ┌────┼────┐ structured
|
|
547
|
+
│ │ │ │ │ │ knowledge
|
|
548
|
+
state cache session store idb sqlite
|
|
549
|
+
│
|
|
550
|
+
observer
|
|
551
|
+
│
|
|
552
|
+
▼
|
|
553
|
+
application → journal → optional sync → cloud
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
The important part is not that memorio has many layers — it's that **you do not have to use them all**.
|
|
557
|
+
|
|
558
|
+
## Sneak Peek
|
|
559
|
+
|
|
560
|
+
### Making a `state` slice persistent
|
|
561
|
+
|
|
562
|
+
`state` is reactive but not persistent; `store` is persistent but not reactive. `persist()` bridges the two — it restores the value from `store` and then keeps the selected state path synchronized with it:
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
import 'memorio'
|
|
566
|
+
|
|
567
|
+
state.user = { name: 'Sara' }
|
|
568
|
+
|
|
569
|
+
const off = persist('state.user') // restore from store, then persist changes
|
|
570
|
+
|
|
571
|
+
// later, e.g. on component unmount
|
|
572
|
+
off()
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
`persist()` uses the same path syntax as `observer()`.
|
|
576
|
+
|
|
577
|
+
Nested paths such as `state.user.preferences.theme` are supported, provided the intermediate objects already exist in `state`.
|
|
578
|
+
|
|
579
|
+
In environments without `localStorage` (Node/Deno/edge), `store` falls back to memory. `persist()` still works there, but persistence does not survive process or runtime restarts.
|
|
455
580
|
|
|
456
581
|
## License
|
|
457
582
|
|