memorio 4.8.0 โ†’ 4.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,672 +1,377 @@
1
1
  # ๐Ÿง  memorio
2
2
 
3
- ![banner](https://raw.githubusercontent.com/passariello/container/refs/heads/main/memorio/banner.svg)
4
-
5
- [![npm version](https://img.shields.io/npm/v/memorio.svg)](https://npmjs.com/package/memorio)
6
- [![npm downloads](https://img.shields.io/npm/dm/memorio.svg)](https://npmjs.com/package/memorio)
7
- [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-gray?logo=node.js)](https://nodejs.org)
8
- ![Browser](https://img.shields.io/badge/Browser-Chrome%20/%20Firefox%20/%20Safari-gray?logo=google-chrome)
9
- ![Deno](https://img.shields.io/badge/Deno-compatible-gray?logo=deno)
10
- ![Edge Workers](https://img.shields.io/badge/Edge%20Workers-compatible-gray)
11
- ![TypeScript](https://img.shields.io/badge/TypeScript-native-gray?logo=typescript)
12
- ![React](https://img.shields.io/badge/React-compatible-gray?logo=react)
13
- ![Tests](https://img.shields.io/badge/tests-101%20passed-green)
14
- ![License](https://img.shields.io/badge/License-MIT-gray)
15
-
16
- ### One import. Global state, persistence, and a IndexedDB layer - done.
17
-
18
- ```javascript
3
+ **Local-first memory for JavaScript.**
4
+ One import. Global state, local persistence, SQLite, semantic memory, optional sync.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/memorio.svg)](https://www.npmjs.com/package/memorio)
7
+ [![npm downloads](https://img.shields.io/npm/dm/memorio.svg)](https://www.npmjs.com/package/memorio)
8
+ [![Socket Badge](https://socket.dev/api/badge/npm/package/memorio)](https://socket.dev/npm/package/memorio)
9
+ [![Known Vulnerabilities](https://snyk.io/test/npm/memorio/badge.svg)](https://snyk.io/test/npm/memorio)
10
+ [![zero deps](https://img.shields.io/badge/dependencies-0-brightgreen)](#security)
11
+ [![license](https://img.shields.io/npm/l/memorio.svg)](#license)
12
+
13
+ ```ts
19
14
  import 'memorio'
20
15
 
21
- state.user = { name: 'Sara' } // reactive, everywhere, instantly
16
+ state.user = { name: 'Sara', role: 'admin' }
17
+ state.counter++
22
18
  ```
23
19
 
24
- No provider tree. No reducers. No actions to dispatch before you can change a number. If you've ever set up a global store for an app that just needed *"a value, shared, that updates the UI when it changes,"* memorio is the toolkit for that thirty-second job - and it keeps growing with you: session storage, a cache, typed IndexedDB tables, an observer system, a React hook, devtools, and a change logger, all sharing one mental model.
20
+ No provider tree. No reducers. No actions. No boilerplate.
21
+ Just data that exists where your application needs it โ€” and grows with it.
25
22
 
26
23
  ---
27
24
 
28
- ## Table of Contents
29
-
30
- 1. [Is this for you?](#is-this-for-you)
31
- 2. [Installation](#installation)
32
- 3. [Quick Start](#quick-start)
33
- 4. [API Reference](#api-reference)
34
- 5. [Typed Stores](#typed-stores)
35
- 6. [Schema Validation](#schema-validation)
36
- 7. [Cross-Platform Behavior](#cross-platform-behavior)
37
- 8. [Context Isolation (multi-tenant)](#context-isolation-multi-tenant)
38
- 9. [Security](#security)
39
- 10. [License](#license)
25
+ ## Table of contents
26
+
27
+ - [Why memorio](#why-memorio)
28
+ - [Which layer should I use](#which-layer-should-i-use)
29
+ - [Install](#install)
30
+ - [Quick start](#quick-start)
31
+ - [Observing changes](#observing-changes)
32
+ - [The layers](#the-layers) โ€” state ยท store ยท session ยท cache ยท idb ยท sqlite ยท memory
33
+ - [React integration](#react-integration)
34
+ - [Typed state & schema validation](#typed-state--schema-validation)
35
+ - [Local-first sync](#local-first-sync)
36
+ - [Cross-platform support](#cross-platform-support)
37
+ - [Security](#security)
38
+ - [Honest limitations](#honest-limitations)
39
+ - [When to use something else](#when-to-use-something-else)
40
+ - [Design philosophy](#design-philosophy)
41
+ - [License](#license)
40
42
 
41
43
  ---
42
44
 
43
- ## Is this for you?
44
-
45
- memorio is built for speed of thought: you think "I need this value everywhere," you write one line, it works. That's the entire pitch - and it's a real one, not a rewritten complaint about how much Redux boilerplate you've had to write.
45
+ ## Why memorio
46
46
 
47
- **Reach for memorio when:**
48
- - You want state that's simply *there* - no store setup, no provider wrapping your app.
49
- - You want `localStorage`, `sessionStorage`, and `IndexedDB` behind one consistent API instead of learning three.
50
- - You're prototyping, building an internal tool, or shipping something small-to-medium where iteration speed matters more than architectural ceremony.
47
+ Modern apps usually split their data across systems that don't talk to each other: a state manager, `localStorage`, `sessionStorage`, IndexedDB, a database, a cache, maybe an AI memory layer, maybe a sync layer on top. Each with its own API, its own mental model, its own edge cases.
51
48
 
52
- **Reach for something else when:**
53
- - You need every state path statically type-checked against every observer - memorio's `state.foo.bar` and `observer('state.foo.bar', ...)` are connected by a string, not by the compiler, so a rename won't be caught for you. However, if you want compiler-checked state access, use `memorio.typed<T>()` (see [Typed Stores](#typed-stores)) which gives you full static types on the global `state` proxy. For runtime validation as well, combine with `memorio.registerSchema()` (see [Schema Validation](#schema-validation)).
54
- - You need Redux-style middleware, action logs, or time-travel debugging as a hard requirement for a large team - those tools exist because that discipline solves real problems at scale, not because their authors enjoy boilerplate. Memorio logs through `memorio.logger` and inspects via `memorio.devtools`, but doesn't have Redux DevTools time-travel.
55
- - Your isolation requirements are a security boundary, not a convenience - see [Context Isolation](#context-isolation-multi-tenant) before you rely on it for that.
49
+ memorio gives these concerns **one runtime and one mental model** โ€” without forcing you to use all of it.
56
50
 
57
- Pick the right tool. memorio is at its best when the answer to "how much state architecture do I need here?" is genuinely "not much."
58
-
59
- ---
60
-
61
- ## Installation
51
+ | Layer | Purpose | Maturity |
52
+ |---|---|---|
53
+ | `state` | reactive volatile application state | Stable |
54
+ | `cache` | transient runtime data | Stable |
55
+ | `session` | session-scoped persistence | Stable |
56
+ | `store` | persistent key/value data | Stable |
57
+ | `idb` | durable structured browser data | Stable |
58
+ | `sqlite` | relational data & SQL | Beta |
59
+ | `memory` | semantic app/agent memory | Beta |
60
+ | `journal` | local-first operation history | Beta |
61
+
62
+ Start with `state`. Add the rest only when your app actually needs it.
63
+
64
+ ## Which layer should I use
65
+
66
+ ```text
67
+ Does the UI need to react automatically to changes?
68
+ โ”‚
69
+ โ”œโ”€ Yes โ†’ state, or a memory-backed reactive slice
70
+ โ”‚
71
+ โ””โ”€ No, I just need to store/retrieve a value
72
+ โ”‚
73
+ โ”œโ”€ Survive a reload?
74
+ โ”‚ โ”œโ”€ No โ†’ cache
75
+ โ”‚ โ”œโ”€ This tab only โ†’ session
76
+ โ”‚ โ””โ”€ Yes, indefinitelyโ†’ store (small) or idb (larger/structured)
77
+ โ”‚
78
+ โ”œโ”€ Need relations, joins, SQL? โ†’ sqlite
79
+ โ””โ”€ Is this "knowledge" the app reasons
80
+ about (confidence, source, expiry)? โ†’ memory
81
+ ```
82
+
83
+ ## Install
62
84
 
63
85
  ```bash
64
86
  npm i memorio
65
- # pnpm add memorio
66
- # yarn add memorio
67
-
68
- # Optional - only if you use the React hook
69
- npm i react react-dom
70
-
71
- # Optional - only if you want a local sql.js for `sqlite` (default uses CDN)
72
- npm i sql.js
73
87
  ```
74
88
 
75
- ---
89
+ Optional peers, only loaded when used:
76
90
 
77
- ## Quick Start
78
-
79
- ### Global style - the whole point of memorio
80
-
81
- ```typescript
82
- import 'memorio' // once, at your entry point
83
-
84
- state.user = { name: 'Sara', role: 'admin' }
85
- state.counter++
86
-
87
- useObserver(
88
- () => console.debug('user changed:', state.user),
89
- [state.user]
90
- )
91
- ```
92
-
93
- ### Named imports - same instances, explicit about it
94
-
95
- ```typescript
96
- import { state, store, session, cache, idb, observer, useObserver, dispatch, memorio } from 'memorio'
97
-
98
- state.user = { name: 'Sara' }
99
- store.set('theme', 'dark')
100
- ```
101
-
102
- ```tsx
103
- function Counter() {
104
- const [, forceUpdate] = useReducer(x => x + 1, 0)
105
- useObserver(forceUpdate, [state.counter])
106
- return <div>Count: {state.counter}</div>
107
- }
91
+ ```bash
92
+ npm i react react-dom # React integration
93
+ npm i sql.js # SQLite engine
108
94
  ```
109
95
 
110
- Two styles, one engine underneath - use whichever reads better in your codebase.
111
-
112
- ---
96
+ **Zero production dependencies.** See [Security](#security).
113
97
 
114
- ## API Reference
98
+ ## Quick start
115
99
 
116
- ### `state` - reactive, volatile, Proxy-based
100
+ ```ts
101
+ import 'memorio'
117
102
 
118
- ```javascript
119
103
  state.user = { name: 'Sara', role: 'admin' }
120
104
  const name = state.user.name
105
+ ```
121
106
 
122
- state.list // ['user', 'items', ...]
123
- state.remove('items')
124
- state.removeAll()
107
+ Reactive, in-memory, Proxy-based, globally accessible. Lock a slice you don't want mutated by accident:
125
108
 
126
- // Freeze a slice of state when you need to stop guessing who mutated it
109
+ ```ts
127
110
  state.config = { maxUsers: 100 }
128
111
  state.config.lock()
129
- state.config.maxUsers = 200 // throws: state 'config' is locked
112
+ state.config.maxUsers = 200 // throws
130
113
  state.config.unlock()
131
114
  ```
132
115
 
133
- ### `store` - the value that survives a refresh
134
-
135
- ```javascript
136
- store.set('preferences', { theme: 'dark' })
137
- store.get('preferences') // { theme: 'dark' } or null
138
- store.isPersistent // true when backed by real localStorage
139
- ```
140
-
141
- ### `session` - lives as long as the tab does
142
-
143
- ```javascript
144
- session.set('token', 'user-abc-123')
145
- session.get('token')
146
- ```
147
-
148
- ### `cache` - the fastest thing you own, gone on refresh
149
-
150
- ```javascript
151
- cache.set('temp', computeExpensiveResult())
152
- cache.get('temp')
153
- ```
154
-
155
- ### `idb` - typed, async, structured tables without the ceremony
156
-
157
- ```javascript
158
- await idb.db.create('my-db')
159
- await idb.table.create('my-db', 'users')
160
- await idb.data.set('my-db', 'users', { id: 1, name: 'Sara' })
161
- const user = await idb.data.get('my-db', 'users', 1)
162
- ```
163
-
164
- > IndexedDB is a browser-only primitive - see [Cross-Platform Behavior](#cross-platform-behavior) for what happens off the browser.
165
-
166
- ### `sqlite` - in-memory SQLite, zero config
116
+ Prefer explicit imports over the global? Same runtime either way:
167
117
 
168
- `memorio.sqlite` is an optional, lazily-loaded SQLite engine backed by [`sql.js`](https://sql.js.org) (the optional `sql.js` npm package is **not** a build-time dependency). It runs entirely in memory โ€” ideal for ad-hoc SQL, structured queryable data, and local relational lookups. The engine is fetched on first use and never loaded when you don't touch `sqlite`.
169
-
170
- ```javascript
171
- import 'memorio' // or: import { sqlite } from 'memorio'
172
-
173
- await sqlite.ready // waits until the sql.js engine is loaded (or rejected)
174
- sqlite.db.support() // true when the current platform can run sql.js
175
- ```
176
-
177
- Bases (databases) are addressed by name, like `idb`:
178
- const SQL = await sqlite.db.getSQL() // the underlying sql.js engine (loaded once, cached)
179
- sqlite.db.version() // engine version string, or null before load
180
- ```
181
-
182
- Bases (databases) are addressed by name, like `idb`. `sqlite.db.create('app')` opens (or creates) an in-memory database, while `sqlite.db.get('app')` retrieves an already-open handle (`sqlite.db.list/delete/size` round out the lifecycle):
183
-
184
- ```javascript
185
- await sqlite.db.create('app') // in-memory database handle
186
- await sqlite.query.run('app', 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)')
187
- ``` `sqlite.data` gives a small shortcut layer, and `sqlite.query` runs SQL:
188
-
189
- ```javascript
190
- // Write (shortcut over raw prepare/bind)
191
- await sqlite.data.set('app', 'users', { id: 1, name: 'Sara' })
192
- await sqlite.data.get('app', 'users', 1) // { id: 1, name: 'Sara' }
193
-
194
- // SQL
195
- await sqlite.query.run('app', 'CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)')
196
- const rows = await sqlite.query.select('app', 'SELECT * FROM items', [1])
197
-
198
- // Portability: export / import a whole base as a binary blob
199
- const blob = await sqlite.db.export('app')
200
- await sqlite.db.import('app', blob)
201
-
202
- // Clean up
203
- await sqlite.db.delete('app')
204
- ```
205
-
206
- #### Loading the engine
207
-
208
- The default loader injects the sql.js UMD build (`sql-wasm-browser.js`) from the
209
- jsDelivr CDN as a classic `<script>`; on load it exposes the `initSqlJs` factory
210
- on `globalThis`, which memorio then calls with your `locateFile`. This avoids a
211
- bare `import('sql.js')` so bundlers never resolve the optional `sql.js`
212
- dependency at build time:
213
-
214
- ```javascript
215
- // Zero config: CDN <script> loader (default)
216
- await sqlite.ready
217
- ```
218
-
219
- If you prefer a local/bundled `sql.js` (offline, private network, or to pin a
220
- version), install it and supply your own loader:
221
-
222
- ```bash
223
- npm i sql.js # optional peer
224
- ```
225
-
226
- ```javascript
227
- import { sqlite } from 'memorio'
228
-
229
- sqlite.config({ loader: () => import('sql.js') }) // local/npm build of sql.js
230
- await sqlite.ready
231
- ```
232
-
233
- You can also point the WASM `locateFile` step at your own CDN/base via
234
- `sqliteWasmBase` on `globalThis`, or pass a custom `locateFile` through
235
- `sqlite.config({ locateFile })`:
236
-
237
- ```javascript
238
- globalThis.sqliteWasmBase = 'https://your-cdn.example.com/sql.js/dist/'
239
- sqlite.config({ locateFile: (file) => `https://your-cdn.example.com/sql.js/dist/${file}` })
118
+ ```ts
119
+ import { state, store, session, cache, idb, sqlite, memorio } from 'memorio'
240
120
  ```
241
121
 
242
- When the engine can't load (e.g. no WASM support), `sqlite.ready` rejects, `sqlite._disabled` becomes `true`, and `sqlite._warning` explains why โ€” `db.support()` simply returns `false` so you can degrade gracefully.
243
-
244
- - `sqlite.config({ persistence: true })` / `sqlite.db.create('app', { persistence: true })` snapshot a database to `store` (localStorage, namespaced) and restore it on reopen โ€” sql.js databases are in-memory by default; see [SQLite docs โ€“ Persistence & dev download](markdown/SQLITE.md#example-5-persistence--dev-download).
245
-
246
- See [SQLite docs](markdown/SQLITE.md) for the full reference.
122
+ ## Observing changes
247
123
 
248
- ### `observer` / `useObserver` - watch a path, react to it
124
+ Reactivity isn't a React add-on โ€” it's built into the runtime. Watch any state path directly, in any environment:
249
125
 
250
- ```javascript
251
- observer('state.user', (newVal, oldVal) => {
252
- console.debug('user changed:', newVal, oldVal)
126
+ ```ts
127
+ observer('state.user', (next, previous) => {
128
+ console.log('user changed:', next, previous)
253
129
  })
254
- ```
255
130
 
256
- > Keys here are plain strings, not statically checked against `state`'s shape. Keep the observer near the code that shapes that state, and grep before you rename.
131
+ // nested paths work too
132
+ observer('state.user.name', callback)
133
+ ```
257
134
 
258
- ### `dispatch` - the event bus underneath it all
135
+ `dispatch` is the event mechanism underneath it, if you need to hook in lower-level:
259
136
 
260
- ```javascript
261
- memorio.dispatch.listen('state.user', (event) => console.debug(event.detail))
137
+ ```ts
138
+ memorio.dispatch.listen('state.user', event => console.debug(event.detail))
262
139
  memorio.dispatch.set('state.user', { detail: { name: 'Sara' } })
263
140
  ```
264
141
 
265
- ### `devtools` - see everything, instantly
142
+ `observer` paths are runtime strings โ€” for compiler-checked access, see [typed state](#typed-state--schema-validation).
266
143
 
267
- ```javascript
268
- memorio.devtools.inspect()
269
- memorio.devtools.stats()
270
- memorio.devtools.exportData()
271
- ```
144
+ React apps get a dedicated hook, `useObserver` โ€” see [React integration](#react-integration).
272
145
 
273
- Pairs with [dphelper-manager](https://chrome.google.com/webstore/detail/dphelper-manager-dev-tool/oppppldaoknfddeikfloonnialijngbk) for visual, time-travel inspection of your global state straight from devtools.
146
+ ## The layers
274
147
 
275
- ### `logger` - a black box for every change
148
+ ### `store` โ€” persistent key/value
276
149
 
277
- ```javascript
278
- memorio.logger.configure({ enabled: true, logToConsole: true })
279
- memorio.logger.getHistory()
150
+ ```ts
151
+ store.set('preferences', { theme: 'dark' })
152
+ const preferences = store.get('preferences')
280
153
  ```
281
154
 
282
- > It logs *everything* written to `state`/`store`/`session` - including anything sensitive you put there. Great for debugging, but don't leave it on unconditionally in production if secrets or PII pass through your state. See [Security](#security).
283
-
284
- ---
285
-
286
- ## Typed Stores
287
-
288
- `memorio.typed<T>()` returns the global `state` proxy cast to a TypeScript type `T`, giving you **compile-time** type safety on every access and mutation. The returned object is the *exact same* Proxy as `globalThis.state` โ€” no duplication, no overhead.
289
-
290
- ```typescript
291
- import 'memorio'
292
-
293
- interface AppState {
294
- user: { name: string; age: number; email: string }
295
- theme: 'light' | 'dark'
296
- items: string[]
297
- }
298
-
299
- const app = memorio.typed<AppState>()
300
-
301
- // Type-checked at compile time:
302
- app.user = { name: 'Sara', age: 30, email: 'sara@test.com' }
303
- app.theme = 'dark'
304
-
305
- // TypeScript errors:
306
- // app.user = { name: 42 } // age missing, name wrong type
307
- // app.theme = 'purple' // not a valid literal
308
- ```
155
+ Backed by `localStorage` in the browser (`store.isPersistent === true`); falls back to memory elsewhere.
309
156
 
310
- The same data is accessible via the global `state` proxy โ€” `app` and `state` are identical instances:
157
+ ### `session` โ€” follows the tab
311
158
 
312
- ```typescript
313
- state.user = { name: 'Sara', age: 30, email: 'sara@test.com' }
314
- app.user.name // 'Sara' โ€” same Proxy
159
+ ```ts
160
+ session.set('token', 'user-abc-123')
315
161
  ```
316
162
 
317
- For runtime safety (rejecting invalid values even when TypeScript isn't checking), combine with [Schema Validation](#schema-validation) below.
318
-
319
- See [Typed Stores docs](markdown/TYPED.md) for full examples.
320
-
321
- ---
322
-
323
- ## Schema Validation
324
-
325
- `memorio.registerSchema(path, schema)` lets you register runtime validators for state paths. When a write to `state.somePath = value` violates a registered schema, the write is **rejected** before the value is stored.
163
+ Backed by `sessionStorage`. Good for auth state, wizards, tab-scoped data.
326
164
 
327
- ```typescript
328
- import 'memorio'
165
+ ### `cache` โ€” volatile, fast
329
166
 
330
- memorio.registerSchema('user', {
331
- type: 'object',
332
- required: ['name', 'email'],
333
- properties: {
334
- name: { type: 'string', min: 1 },
335
- email: { type: 'string', pattern: /^[^@]+@[^@]+$/ },
336
- age: { type: 'number', min: 0, max: 150 }
337
- }
338
- })
339
-
340
- state.user = { name: 'Sara', email: 'sara@test.com', age: 30 } // accepted
341
- state.user = { name: 'Sara' } // rejected: missing 'email'
342
- state.user = { name: 42 } // rejected: wrong type
167
+ ```ts
168
+ cache.set('expensive-result', computeExpensiveResult())
343
169
  ```
344
170
 
345
- Custom validator functions are also supported:
171
+ Disappears when the runtime disappears. No persistence guarantee, ever.
346
172
 
347
- ```typescript
348
- memorio.registerSchema('theme', (val) => {
349
- return val === 'light' || val === 'dark'
350
- ? true
351
- : 'theme must be "light" or "dark"'
352
- })
173
+ ### `idb` โ€” durable, structured
353
174
 
354
- state.theme = 'purple' // rejected: "theme must be light or dark"
355
- state.theme = 'dark' // accepted
175
+ ```ts
176
+ await idb.db.create('app')
177
+ await idb.table.create('app', 'users')
178
+ await idb.data.set('app', 'users', { id: 1, name: 'Sara' })
179
+ const user = await idb.data.get('app', 'users', 1)
356
180
  ```
357
181
 
358
- Manual validation without writing:
182
+ Check before relying on it in portable code: `memorio.getCapabilities()`.
359
183
 
360
- ```typescript
361
- memorio.validate('user', { name: 'Sara', email: 'sara@test.com' })
362
- // { valid: true }
184
+ ### `sqlite` โ€” a real local SQL engine
363
185
 
364
- memorio.validate('user', { name: 'Sara' })
365
- // { valid: false, errors: ["user: missing required property 'email'"] }
366
- ```
186
+ ```ts
187
+ await sqlite.ready
188
+ await sqlite.db.create('app')
367
189
 
368
- Manage registered schemas:
190
+ await sqlite.query.run('app', `
191
+ CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, role TEXT)
192
+ `)
193
+ await sqlite.query.run('app', `INSERT INTO users (name, role) VALUES (?, ?)`, ['Sara', 'admin'])
369
194
 
370
- ```typescript
371
- memorio.listSchemas() // ['user', 'theme']
372
- memorio.unregisterSchema('theme') // removes the validator
195
+ const admins = await sqlite.query.select('app', `SELECT * FROM users WHERE role = ?`, ['admin'])
373
196
  ```
374
197
 
375
- **Typed + Schema** โ€” combine both for full safety:
198
+ Runs **in memory by default**, powered by `sql.js` (lazy-loaded โ€” see [loading strategies](#cross-platform-support)). Enable persistence explicitly when you need it:
376
199
 
377
- ```typescript
378
- interface AppState {
379
- user: { name: string; email: string }
380
- theme: 'light' | 'dark'
381
- }
382
-
383
- const app = memorio.typed<AppState>()
384
-
385
- memorio.registerSchema('user', {
386
- type: 'object',
387
- required: ['name', 'email'],
388
- properties: {
389
- name: { type: 'string', min: 1 },
390
- email: { type: 'string', pattern: /^[^@]+@[^@]+$/ }
391
- }
392
- })
393
-
394
- app.user = { names: 'Sara' } // โŒ TS: wrong shape
395
- app.user = { name: '', email: '' } // โŒ TS passes, โŒ runtime: name too short
396
- app.user = { name: 'Sara', email: 'sara@test.com' } // โœ… both pass
200
+ ```ts
201
+ await sqlite.db.create('app', { persistence: true })
397
202
  ```
398
203
 
399
- See [Schema Validation docs](markdown/SCHEMA.md) for the full schema definition reference.
400
-
401
- ---
402
-
403
- ## Memory System
204
+ > โš ๏ธ Persistence serializes the **entire** database on each flush โ€” it is not incremental. Fine for small/medium data; for larger datasets, persist deliberately after a batch of writes, not on every mutation. See [Honest limitations](#honest-limitations).
404
205
 
405
- `memorio.memory` provides a semantic memory layer โ€” a key/value store with **type safety**, **TTL**, **confidence scoring**, **tagging**, and **scope-based persistence**. It's designed for AI agents and applications that need structured, queryable memory with lifecycle management.
206
+ ### `memory` โ€” semantic application memory
406
207
 
407
- ### Core API
208
+ The layer that makes memorio more than a state manager. Structured memory with type, confidence, TTL, tags, source, scope, and a real lifecycle:
408
209
 
409
210
  ```ts
410
- // remember(key, value, options)
411
211
  await memorio.memory.remember('user.language', 'Italian', {
412
212
  type: 'preference',
413
213
  confidence: 0.92,
414
214
  scope: 'local',
415
- ttl: null, // never expires
416
- tags: ['ui', 'user'],
417
- source: 'conversation'
215
+ tags: ['user', 'ui'],
216
+ source: 'conversation',
418
217
  })
419
218
 
420
- // recall(key, options)
421
- const lang = await memorio.memory.recall('user.language')
422
- // โ†’ 'Italian'
423
-
424
- // update(key, value, options) โ€” creates a superseded version of the old entry
425
- await memorio.memory.update('user.language', 'English', { confidence: 0.95 })
426
-
427
- // forget(key) โ€” permanently deletes
428
- await memorio.memory.forget('user.language')
219
+ const language = await memorio.memory.recall('user.language')
429
220
  ```
430
221
 
431
- ### Scopes
222
+ Updates don't overwrite โ€” they **supersede**, preserving history:
432
223
 
433
- | Scope | Storage | TTL | Cross-session | Size limit |
434
- |---|---|---|---|---|
435
- | `hot` | `state` proxy | โœ… | โŒ | ~5MB (RAM) |
436
- | `session` | `sessionStorage` | โœ… | Tab only | ~5MB |
437
- | `local` | `localStorage` | โœ… | โœ… | ~10MB |
438
- | `durable` | `IndexedDB` | โœ… | โœ… | ~1GB+ |
439
-
440
- Default scope is `local` for values โ‰ค100KB, `durable` for larger values.
224
+ ```ts
225
+ await memorio.memory.update('user.language', 'English', { confidence: 0.95 })
226
+ // old entry โ†’ status: 'superseded' | new entry โ†’ status: 'active'
227
+ ```
441
228
 
442
- ### Context API
229
+ Retrieve what's *relevant*, not everything:
443
230
 
444
231
  ```ts
445
- // context() โ€” returns ranked, relevant memories
446
232
  const context = await memorio.memory.context({
447
233
  tags: 'user',
448
234
  types: ['preference', 'decision'],
449
235
  minConfidence: 0.7,
450
- maxEntries: 10
236
+ maxEntries: 10,
451
237
  })
452
- // โ†’ [{ key, value, relevance, age, confidence, ... }]
453
238
  ```
454
239
 
455
- ### Memory Entry Model
456
-
457
- Each memory is stored as a structured entry:
240
+ > โ„น๏ธ **"Semantic" here means structured, not embedding-based.** `memory.context()` ranks by tags, type, confidence and recency โ€” there's no vector similarity search under the hood (yet โ€” see [roadmap](#honest-limitations)). Need true meaning-based retrieval over free text? Pair this layer with your own embedding store and use `memorio.memory` for the lifecycle (confidence, TTL, supersession) on top.
458
241
 
459
- ```ts
460
- {
461
- id: string // unique identifier
462
- key: string // path-like key (e.g. "user.preference.theme")
463
- value: any // the stored data
464
- type: MemoryType // 'fact' | 'preference' | 'decision' | 'task' | 'context'
465
- confidence: number // 0.0โ€“1.0
466
- scope: MemoryScope // 'hot' | 'session' | 'local' | 'durable'
467
- ttl?: number | null // milliseconds (null = never expires)
468
- tags: string[] // for filtering
469
- source?: string // where this memory came from
470
- status: MemoryStatus // 'active' | 'obsolete' | 'superseded'
471
- createdAt: number // timestamp
472
- lastConfirmedAt: number
473
- supersededId?: string | null
474
- }
475
- ```
242
+ `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.
476
243
 
477
- ### Memory Lifecycle
244
+ ## React integration
478
245
 
479
- | Operation | Behavior |
480
- |---|---|
481
- | `remember(key, newValue)` when entry exists | Old entry โ†’ `superseded` status, new entry โ†’ `active` |
482
- | `recall(key)` | Returns active value; expired entries return `null` unless `includeObsolete: true` |
483
- | `update(key, value)` | Creates superseded copy + updated active entry |
484
- | Entry with `ttl` expires | Status โ†’ `obsolete` (via `forgetExpired()`) |
485
- | `clear()` | Wipes all memories + index |
246
+ React is an integration, not a requirement โ€” `useObserver` is a thin bridge onto the same `observer` mechanism from above:
486
247
 
487
- See [Memory docs](markdown/MEMORY.md) for full reference.
248
+ ```tsx
249
+ function Counter() {
250
+ const [, forceUpdate] = useReducer(x => x + 1, 0)
251
+ useObserver(forceUpdate, [state.counter])
252
+ return <div>{state.counter}</div>
253
+ }
254
+ ```
488
255
 
489
- ---
256
+ ## Typed state & schema validation
490
257
 
491
- ## Synchronization & Cloud (optional)
258
+ TypeScript types for compile-time safety:
492
259
 
493
- `memorio.memory` is **local-first**. The data is created and served from the
494
- device; the cloud is only ever a **transport/persistence provider**, never the
495
- source of truth. Enabling sync does not replace local storage โ€” it *mirrors* it.
260
+ ```ts
261
+ interface AppState {
262
+ user: { name: string; age: number; email: string }
263
+ theme: 'light' | 'dark'
264
+ }
496
265
 
497
- ```
498
- memorio
499
- โ”‚
500
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
501
- โ”‚ Memory Engine โ”‚
502
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
503
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
504
- โ–ผ โ–ผ โ–ผ
505
- local SQLite cloud
506
- memory durable sync
266
+ const app = memorio.typed<AppState>()
267
+ app.theme = 'dark'
268
+ app.theme = 'purple' // โŒ TypeScript error
507
269
  ```
508
270
 
509
- ### Configuring a backend
271
+ `app === state` โ€” same Proxy, no duplicated store.
510
272
 
511
- Sync is **opt-in**. You supply an application-owned `provider` that knows how to
512
- talk to your backend (REST, WebSocket, Supabase, a custom agent server, a PostgreSQL
513
- database, etc.). **Memorio never handles credentials** โ€” authentication and
514
- authorization live in your backend/provider (OWASP A01: Broken Access Control):
273
+ Runtime validation for values crossing trust boundaries:
515
274
 
516
275
  ```ts
517
- memorio.memory.configure({
518
- namespace: 'user:123:device:abc', // tenant/user/device โ€” partitions the journal
519
- provider: { // application-owned transport
520
- push(ops) { return fetch('/api/sync', { method: 'POST', body: JSON.stringify(ops), headers: auth }) }
521
- pull(since) { return fetch(`/api/sync?since=${since}`).then(r => r.json()) }
276
+ memorio.registerSchema('user', {
277
+ type: 'object',
278
+ required: ['name', 'email'],
279
+ properties: {
280
+ name: { type: 'string', min: 1 },
281
+ email: { type: 'string', pattern: /^[^@]+@[^@]+$/ },
522
282
  },
523
- auto: true // auto-replay pending ops on focus/online (default true)
524
283
  })
525
284
 
526
- await memorio.memory.ready // waits until the local journal substrate is chosen
285
+ state.user = { name: 'Sara' } // rejected โ€” missing required "email"
527
286
  ```
528
287
 
529
- Once configured, `memorio.memory` records every operation locally and keeps it
530
- until the provider acknowledges it.
288
+ ## Local-first sync
531
289
 
532
- ### The local journal
290
+ The local application owns its data; the cloud is optional transport.
533
291
 
534
- Every mutation (`remember`, `update`, `forget`, `expire`, `confirm`, `supersede`)
535
- is written to a local, **namespaced** operation journal:
292
+ memorio syncs **operations** (`remember`, `update`, `forget`, `expire`, `confirm`, `supersede`), not database dumps, via a local journal that survives network failure:
536
293
 
537
294
  ```ts
538
- await memory.remember('user.language', 'Italian', { scope: 'local' })
539
-
540
- memory.journal.pending() // -> [{ id, key:'user.language', value:'Italian', operation:'remember', sync:'pending', version: 1, updatedAt: ... }]
541
- memory.journal.markSynced([id])
542
- memory.journal.replay() // pushes pending() to provider.provider, marks synced, optional pull+apply
543
- memory.journal.status() // 'sqlite' | 'store' (substrate in use)
295
+ memorio.memory.configure({
296
+ namespace: 'user:123:device:abc',
297
+ provider: {
298
+ push: (ops) => fetch('/api/sync', { method: 'POST', body: JSON.stringify(ops) }),
299
+ pull: (since) => fetch(`/api/sync?since=${since}`).then(r => r.json()),
300
+ },
301
+ auto: true,
302
+ })
544
303
  ```
545
304
 
546
- The journal stores the full entry plus `operation`, `version`, `updatedAt` and
547
- `sync` status, so conflicts can be resolved when connectivity returns.
548
-
549
- ### Substrates
550
-
551
- The journal is persisted on `store` (localStorage / in-memory Map fallback) โ€” **not** on a sql.js database, because sql.js databases are volatile (in-memory) and would lose pending operations on reload:
552
-
553
- | Substrate | Used for | Notes |
554
- |---|---|---|
555
- | `store` (localStorage) | sync journal | persistent across reloads; namespaced; Map fallback in Node |
556
- | `sqlite` (sql.js) | ad-hoc SQL / value storage | in-memory by default โ€” use `persistence: true` to snapshot to `store` |
557
- | `idb` (IndexedDB) | `memorio.memory` durable scope values | persistent; persistent across reloads |
558
-
559
- The `sqlite.db.download(name)` dev helper also triggers a browser `.sqlite` download of an in-memory database on demand.
560
-
561
- ### Sync = operations, not database dumps
562
-
563
- Memorio syncronizza **operazioni di memoria**, never a raw database dump. When
564
- two devices diverge, conflict resolution is based on `confidence`,
565
- `lastConfirmedAt`, `source`, `version` and `scope` โ€” not just "last write wins":
305
+ Conflict resolution defaults to *higher confidence wins, then more recent `lastConfirmedAt`* โ€” override it when you need a different rule:
566
306
 
567
307
  ```ts
568
- // Device A: user.language = Italian, confidence 0.92
569
- // Device B: user.language = English, confidence 0.61
570
- // โ†’ the higher-confidence entry wins locally; the provider decides for shared.
571
- ```
572
-
573
- The `namespace` (tenant / user / device) is the single partition key: local
574
- journal reads and writes are scoped to it, and they can never cross namespaces โ€”
575
- a client holding a fake/forged namespace simply sees its own empty journal.
576
-
577
- ### Scopes (isolation, not a security boundary)
578
-
579
- ```
580
- scope: 'device' // only this browser/device
581
- scope: 'user' // follows the user across devices (via sync)
582
- scope: 'shared' // shared across users/tenant
308
+ memorio.memory.configure({
309
+ resolveConflict(local, remote) {
310
+ if (remote.source === 'user-correction') return remote
311
+ return local.confidence >= remote.confidence ? local : remote
312
+ },
313
+ })
583
314
  ```
584
315
 
585
- `scope: 'user'`/`'shared'` require a provider + namespace. `scope: 'device'`
586
- is local only. As with `memorio.createContext`, **scoping is a naming convention,
587
- not a security boundary** โ€” enforce real isolation in your backend.
588
-
589
- ### Security posture
590
-
591
- - **NIST SP 800-53 / OWASP**: no credentials, tokens, or secrets are read from or
592
- stored by `memorio`; sensitive state you place in `state`/`store`/`session`/`idb`
593
- is not encrypted by memorio (see [Security](#security)).
594
- - **Namespace isolation**: journal reads/writes are keyed by
595
- `namespace:id` at the storage layer; there is no API to enumerate or open
596
- another namespace's journal (defense-in-depth).
597
- - **No dynamic code**: journal entries are strictly JSON-round-tripped and
598
- size-capped (10 MB/entry); no `eval`/template-injection of provider data.
599
- - **Trust boundary**: the provider/backend owns authentication, authorization,
600
- and remote-side conflict resolution. Memorio owns the local durable copy and
601
- the operation log; it surfaces `conflict`/`error` rows via `journal.pending()`.
602
- - **NSA/CISA advice (data-at-rest/secrets)**: if you persist `state`/`store`
603
- server-side or ship user data through your backend, encrypt it server-side with
604
- keys you manage; memorio treats the local journal as untrusted-from-the-browser
605
- and does not attest its own integrity.
606
-
607
- See [SQLite docs](markdown/SQLITE.md), [Memory docs](markdown/MEMORY.md), and the [Synchronization & Cloud guide](markdown/SYNC.md).
608
-
609
- ---
610
-
611
- ## Cross-Platform Behavior
316
+ This resolver only settles *client-side* divergence between what memorio has seen locally โ€” the provider is still responsible for the final server-side policy.
612
317
 
613
- memorio runs everywhere JavaScript does - but "everywhere" means different guarantees in different places, and we'd rather tell you now than have you find out at 2am:
318
+ ## Cross-platform support
614
319
 
615
320
  | API | Browser | Node.js | Deno | Edge / Workers |
616
- |---|---|---|---|---|
617
- | `state` | Proxy in memory | Proxy in memory | Proxy in memory | Proxy in memory |
618
- | `observer` / `useObserver` | โœ… | โœ… | โœ… | โœ… |
619
- | `cache` | โœ… in memory | โœ… in memory | โœ… in memory | โœ… in memory |
620
- | `store` | `localStorage` | `Map` fallback - **not durable across restarts** | `Map` fallback - not durable | `localStorage` where available, else `Map` |
621
- | `session` | `sessionStorage` | `Map` fallback - not durable | `Map` fallback - not durable | `sessionStorage` where available, else `Map` |
622
- | `idb` | โœ… `IndexedDB` | โŒ not available | โŒ not available | โš ๏ธ check `getCapabilities()` |
623
- | `sqlite` | โœ… `sql.js` (lazy, CDN by default) | โŒ not available | โŒ not available | โš ๏ธ check `sqlite.db.support()` |
624
- | `devtools` | โœ… | โŒ | โŒ | โš ๏ธ |
625
-
626
- Same API top to bottom - that's the promise. But if your server code leans on `store.get(...)` surviving a redeploy, know that on Node/Deno it won't; the fallback is an in-memory cache with the same shape, not durable storage.
627
-
628
- ```javascript
629
- memorio.isBrowser()
630
- memorio.isNode()
631
- memorio.isDeno()
632
- memorio.isEdge()
321
+ |---|---:|---:|---:|---:|
322
+ | `state` / `observer` / `cache` | โœ… | โœ… | โœ… | โœ… |
323
+ | `store` / `session` | persistent | memory fallback | memory fallback | capability-dependent |
324
+ | `idb` | โœ… | โŒ | โŒ | capability-dependent |
325
+ | `sqlite` | โœ… | โŒ | โŒ | capability-dependent |
326
+ | `devtools` | โœ… (dev-only) | โŒ | โŒ | capability-dependent |
633
327
 
328
+ ```ts
634
329
  memorio.getCapabilities()
635
- // { platform: 'browser', hasLocalStorage: true, hasIndexedDB: true, ... }
330
+ memorio.isBrowser() / isNode() / isDeno() / isEdge()
636
331
  ```
637
332
 
638
- Check `getCapabilities()` before leaning on `idb` or persistent `store`/`session` in code that might run on more than one platform.
639
-
640
- ---
641
-
642
- ## Context Isolation (multi-tenant)
333
+ ## Security
643
334
 
644
- ```javascript
645
- const ctx = memorio.createContext('tenant-123')
335
+ - Zero production dependencies โ€” [verified by Socket.dev](https://socket.dev/npm/package/memorio) and [scanned by Snyk](https://snyk.io/test/npm/memorio)
336
+ - No `eval`, no dynamic code execution, no bundled telemetry
337
+ - Sanitized keys, validated inputs, caught module-boundary errors
338
+ - UUID-based session identifiers, bounded journal entries
646
339
 
647
- ctx.state.user = { name: 'Isolated' }
648
- ctx.store.set('settings', { theme: 'dark' })
340
+ **What memorio does *not* do:** it does not encrypt `state`, `store`, `session`, `idb`, local memory, or SQLite contents. Treat browser storage as client-controlled data. If you handle auth tokens, secrets, or regulated data, bring your own encryption and backend security.
649
341
 
650
- console.debug(state.user) // undefined - separate namespace
651
- ```
342
+ **Contexts, scopes, and namespaces are not security boundaries.** `memorio.createContext('tenant-123')` is for code organization โ€” anything in the same JS runtime can, in principle, reach any context through the memorio API. Real tenant isolation belongs at the backend/auth layer.
652
343
 
653
- Handy for keeping per-tenant or per-request state from colliding in the same process. Under the hood it's a key prefix (`tenant-123-key`) inside the same storage - a naming convention, not a hard memory boundary. Great for a browser tab; if you're running this in a shared Node.js process or an edge isolate that might be reused across requests, make sure tenant IDs can't collide or be forged, and don't treat this as your only isolation layer if the stakes are real.
344
+ ## Honest limitations
654
345
 
655
- ---
346
+ We'd rather tell you where the edges are than have you find them in production.
656
347
 
657
- ## Security
348
+ - **`memory.context()` is rule-based, not embedding-based.** No free-text semantic similarity yet โ€” see [roadmap](#when-to-use-something-else).
349
+ - **SQLite persistence is a full serialize-on-flush**, not incremental. Costly for large datasets if triggered on every write.
350
+ - **Namespaces and contexts are organizational, not authorization boundaries.**
351
+ - **Nothing is encrypted by default**, anywhere.
352
+ - **DevTools are dev-only by runtime detection** (`process.env.NODE_ENV`), not a build-time strip โ€” double-check your bundler actually sets this in production.
658
353
 
659
- - Zero production dependencies.
660
- - No `eval`, no dynamic code execution, no bundled telemetry.
661
- - Session IDs generated via `crypto.randomUUID`.
662
- - Inputs validated, keys sanitized, errors caught at module boundaries.
354
+ ## When to use something else
663
355
 
664
- memorio does **not** encrypt what you put into `store`, `session`, or `idb` - they're thin, fast wrappers over `localStorage`/`sessionStorage`/`IndexedDB`, which store data in the clear on the user's device. Bring your own encryption layer for tokens, secrets, or regulated personal data.
356
+ - **Need strict Redux-style architecture** (action pipelines, middleware, time-travel debugging) โ†’ use a dedicated Redux-style setup.
357
+ - **Need hard security isolation** โ†’ real backend authorization, process isolation, encryption. Don't lean on memorio contexts.
358
+ - **Need durable server storage** โ†’ Node/edge runtimes don't gain browser persistence for free. Use a server database.
359
+ - **Need true semantic/embedding retrieval** โ†’ pair `memorio.memory` with a dedicated embedding store; use memorio for the lifecycle metadata on top.
665
360
 
666
- We build with recognized engineering guidance (NIST SP 800-53 practices) as an input to how we write code - that's a design discipline, not a compliance certificate, and no third-party audit has been performed. Found an issue? Report it privately - see `SECURITY.md` - rather than opening a public issue.
361
+ ## Design philosophy
667
362
 
668
- ---
363
+ 1. **Local first** โ€” the app stays useful when the network disappears.
364
+ 2. **Persistence is incremental** โ€” start with memory, persist only when useful.
365
+ 3. **The cloud is optional** โ€” an extension, never a prerequisite.
366
+ 4. **Choose the right primitive** โ€” don't put relational data in a key/value store, or semantic memory in ordinary state.
367
+ 5. **Memory has meaning** โ€” confidence, source, lifetime, scope, type, history.
368
+ 6. **Tell the truth about boundaries** โ€” volatile, unencrypted, not-a-security-boundary: say so, everywhere it applies.
369
+ 7. **Keep the common case tiny**:
370
+ ```ts
371
+ import 'memorio'
372
+ state.value = 42
373
+ ```
669
374
 
670
375
  ## License
671
376
 
672
- MIT ยฉ [Dario Passariello](https://dario.passariello.ca)
377
+ MIT ยฉ Dario Passariello