memorio 4.9.31 → 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 +327 -330
- 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 +706 -649
- package/index.d.ts +1 -0
- package/index.js +686 -648
- 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 +561 -374
- package/modules/redux.cjs.map +1 -1
- package/modules/redux.js +561 -374
- 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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
> **Status:** Published
|
|
2
|
+
> **Date:** 2026-09-12
|
|
3
|
+
> **Deciders:** Memorio 5.x Core Team
|
|
4
|
+
> **Scope**: API Reference
|
|
5
|
+
> **Standard**: Memorio API Specification v5
|
|
6
|
+
>
|
|
7
|
+
---
|
|
8
|
+
# State - Memorio
|
|
9
|
+
|
|
10
|
+
> ✅ **Universal**: Works in Browser, Node.js, Deno, and Edge Workers
|
|
11
|
+
|
|
12
|
+
State is a reactive global state manager using JavaScript Proxies. It's simple, powerful, and requires no setup. Data persists only in memory during the session.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install memorio
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
import { state } from 'memorio';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That's it. `state` is ready to use.
|
|
25
|
+
|
|
26
|
+
> **Classic `import`**: `state` is also available as a named export from the global entrypoint.
|
|
27
|
+
> `import 'memorio/global'` exposes the same proxy as `globalThis.state`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Examples
|
|
32
|
+
|
|
33
|
+
### Example 1: Basic Usage
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
// Set a value
|
|
37
|
+
state.name = 'Mario';
|
|
38
|
+
state.age = 25;
|
|
39
|
+
|
|
40
|
+
// Get a value
|
|
41
|
+
console.debug(state.name); // "Mario"
|
|
42
|
+
|
|
43
|
+
// Simple object
|
|
44
|
+
state.user = { name: 'Luigi', level: 1 };
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Example 2: Intermediate
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// Array operations
|
|
51
|
+
state.items = [1, 2, 3];
|
|
52
|
+
state.items.push(4);
|
|
53
|
+
console.debug(state.items); // [1, 2, 3, 4]
|
|
54
|
+
|
|
55
|
+
// Nested objects
|
|
56
|
+
state.config = { theme: 'dark', lang: 'en' };
|
|
57
|
+
state.config.theme = 'light';
|
|
58
|
+
|
|
59
|
+
// List all states
|
|
60
|
+
console.debug(state.list);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Example 3: Advanced
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// Lock state to prevent modifications
|
|
67
|
+
state.frozenConfig = { maxUsers: 100 };
|
|
68
|
+
state.frozenConfig.lock();
|
|
69
|
+
// Now state.frozenConfig cannot be modified
|
|
70
|
+
|
|
71
|
+
// Path tracking
|
|
72
|
+
const path = state.user.path;
|
|
73
|
+
console.debug(path.name); // "user"
|
|
74
|
+
console.debug(path.profile.name); // "user.profile"
|
|
75
|
+
|
|
76
|
+
// Get full path as string
|
|
77
|
+
console.debug(state.user.__path); // "state.user"
|
|
78
|
+
|
|
79
|
+
// Protected keys (internal use)
|
|
80
|
+
console.debug(protect); // Array of protected keys
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## API Reference
|
|
86
|
+
|
|
87
|
+
### Properties
|
|
88
|
+
|
|
89
|
+
| Property | Type | Description |
|
|
90
|
+
|----------|------|-------------|
|
|
91
|
+
| `state.list` | Array | Get all current state keys (deep copy) |
|
|
92
|
+
| `state.path` | Object | Get path tracker for current location |
|
|
93
|
+
| `state.__path` | string | Get full path as string |
|
|
94
|
+
|
|
95
|
+
### Methods
|
|
96
|
+
|
|
97
|
+
| Method | Parameters | Description |
|
|
98
|
+
|--------|------------|-------------|
|
|
99
|
+
| `state.remove(key)` | `key: string` | Remove a specific state |
|
|
100
|
+
| `state.removeAll()` | none | Clear all states |
|
|
101
|
+
|
|
102
|
+
### Lock
|
|
103
|
+
|
|
104
|
+
```javascript
|
|
105
|
+
// Lock an object or array
|
|
106
|
+
state.myArray = [1, 2, 3];
|
|
107
|
+
state.myArray.lock();
|
|
108
|
+
|
|
109
|
+
// Now any modification will fail
|
|
110
|
+
state.myArray.push(4); // Error: state 'myArray' is locked
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## How It Works
|
|
116
|
+
|
|
117
|
+
Memorio uses JavaScript `Proxy` to intercept get/set operations on the global `state` object. This allows:
|
|
118
|
+
|
|
119
|
+
1. **Reactivity** - Any change can trigger observers
|
|
120
|
+
2. **Nested objects** - Deep path tracking
|
|
121
|
+
3. **Type safety** - Full TypeScript support
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Platform Notes
|
|
126
|
+
|
|
127
|
+
| Platform | Support | Notes |
|
|
128
|
+
|----------|---------|-------|
|
|
129
|
+
| Browser | ✅ Full | In-memory, lost on refresh |
|
|
130
|
+
| Node.js | ✅ Full | In-memory, lost on restart |
|
|
131
|
+
| Deno | ✅ Full | In-memory, lost on restart |
|
|
132
|
+
| Edge Workers | ✅ Full | In-memory, lost on function cold start |
|
|
133
|
+
|
|
134
|
+
**Note**: In server environments (Node.js/Deno), use `memorio.createContext()` for request isolation.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Best Practices
|
|
139
|
+
|
|
140
|
+
1. Use descriptive keys: `state.userProfile` not `state.up`
|
|
141
|
+
2. Group related data: `state.cart.items` not `state.cartItems`
|
|
142
|
+
3. Lock static config: `state.appConfig.lock()`
|
|
143
|
+
4. Clean up on logout: `state.removeAll()`
|
|
144
|
+
5. Use path tracking for debugging: `state.myData.__path`
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Common Errors
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// Error: protected key
|
|
152
|
+
state._internal = 'value';
|
|
153
|
+
// Output: "key _internal is protected"
|
|
154
|
+
|
|
155
|
+
// Error: locked state
|
|
156
|
+
state.locked = { x: 1 };
|
|
157
|
+
state.locked.lock();
|
|
158
|
+
state.locked.x = 2;
|
|
159
|
+
// Output: "Error: state 'locked' is locked"
|
|
160
|
+
```
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
> **Status:** Published
|
|
2
|
+
> **Date:** 2026-09-12
|
|
3
|
+
> **Deciders:** Memorio 5.x Core Team
|
|
4
|
+
> **Scope**: API Reference
|
|
5
|
+
> **Standard**: Memorio API Specification v5
|
|
6
|
+
>
|
|
7
|
+
---
|
|
8
|
+
# Store - Memorio
|
|
9
|
+
|
|
10
|
+
> 🖥️ **Browser & Edge**: Uses localStorage for persistence
|
|
11
|
+
> ⚙️ **Node.js/Deno**: Falls back to in-memory storage (not persistent)
|
|
12
|
+
|
|
13
|
+
Store provides persistent localStorage management with a simple API. Data survives page refreshes and browser restarts.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install memorio
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import { store } from 'memorio';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> **Classic `import`**: `store` is also available via the global entrypoint.
|
|
26
|
+
> `import 'memorio/global'` exposes the same instance as `globalThis.store`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Quick Examples
|
|
31
|
+
|
|
32
|
+
### Example 1: Basic Usage
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
// Save data
|
|
36
|
+
store.set('username', 'Mario');
|
|
37
|
+
store.set('score', 1500);
|
|
38
|
+
|
|
39
|
+
// Read data
|
|
40
|
+
console.debug(store.get('username')); // "Mario"
|
|
41
|
+
console.debug(store.get('score')); // 1500
|
|
42
|
+
|
|
43
|
+
// Check if using real persistence
|
|
44
|
+
console.debug(store.isPersistent); // true in browser, false in Node.js/Deno
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Example 2: Intermediate
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// Store objects
|
|
51
|
+
store.set('user', { name: 'Luigi', level: 5 });
|
|
52
|
+
const user = store.get('user');
|
|
53
|
+
console.debug(user.name); // "Luigi"
|
|
54
|
+
|
|
55
|
+
// Remove single item
|
|
56
|
+
store.remove('username');
|
|
57
|
+
|
|
58
|
+
// Check size
|
|
59
|
+
const totalSize = store.size();
|
|
60
|
+
console.debug(`${totalSize} bytes`);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Example 3: Advanced
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// Get storage quota (returns Promise<[usage, quota]> in KB)
|
|
67
|
+
const [used, total] = await store.quota();
|
|
68
|
+
console.debug(`Using ${used} out of ${total} KB`);
|
|
69
|
+
|
|
70
|
+
// Get total size in characters
|
|
71
|
+
const size = store.size();
|
|
72
|
+
console.debug(`${size} bytes`);
|
|
73
|
+
|
|
74
|
+
// Clear all data
|
|
75
|
+
store.removeAll();
|
|
76
|
+
// or use alias
|
|
77
|
+
store.clearAll();
|
|
78
|
+
|
|
79
|
+
// Handle errors gracefully
|
|
80
|
+
try {
|
|
81
|
+
store.set('largeData', hugeObject);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error('Storage full:', err);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## API Reference
|
|
90
|
+
|
|
91
|
+
### Methods
|
|
92
|
+
|
|
93
|
+
| Method | Parameters | Returns | Description |
|
|
94
|
+
|--------|------------|---------|-------------|
|
|
95
|
+
| `store.get(name)` | `name: string` | `any` | Get value from storage |
|
|
96
|
+
| `store.set(name, value)` | `name: string, value: any` | `void` | Save value to storage |
|
|
97
|
+
| `store.remove(name)` | `name: string` | `boolean` | Remove single item |
|
|
98
|
+
| `store.delete(name)` | `name: string` | `boolean` | Alias for remove |
|
|
99
|
+
| `store.removeAll()` | none | `boolean` | Clear all storage |
|
|
100
|
+
| `store.clearAll()` | none | `boolean` | Alias for removeAll |
|
|
101
|
+
| `store.size()` | none | `number` | Get total size in characters |
|
|
102
|
+
| `store.quota()` | none | `Promise<[number, number]>` | Get storage usage/quota in KB |
|
|
103
|
+
|
|
104
|
+
### Properties
|
|
105
|
+
|
|
106
|
+
| Property | Type | Description |
|
|
107
|
+
|----------|------|-------------|
|
|
108
|
+
| `store.isPersistent` | `boolean` | `true` if using real localStorage, `false` if in-memory fallback |
|
|
109
|
+
|
|
110
|
+
### Supported Types
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
// All JSON-serializable types work
|
|
114
|
+
store.set('string', 'hello');
|
|
115
|
+
store.set('number', 42);
|
|
116
|
+
store.set('boolean', true);
|
|
117
|
+
store.set('array', [1, 2, 3]);
|
|
118
|
+
store.set('object', { key: 'value' });
|
|
119
|
+
store.set('null', null);
|
|
120
|
+
store.set('undefined', null); // converted to null
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Not Supported
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
// Functions will log an error
|
|
127
|
+
store.set('myFunc', () => {});
|
|
128
|
+
// Output: "It's not secure to store functions."
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Platform Comparison
|
|
134
|
+
|
|
135
|
+
| Feature | Store | Session | Cache | IDB |
|
|
136
|
+
|---------|-------|---------|-------|-----|
|
|
137
|
+
| **Storage** | localStorage | sessionStorage | Memory | IndexedDB |
|
|
138
|
+
| **Lifetime** | Forever | Until tab closes | Until refresh | Forever |
|
|
139
|
+
| **Capacity** | ~5-10 MB | ~5-10 MB | Unlimited | 50+ MB |
|
|
140
|
+
| **Platform** | Browser/Edge | Browser/Edge | All | Browser |
|
|
141
|
+
| **Persistence** | ✅ true | N/A | ❌ false | ✅ true |
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## How It Works
|
|
146
|
+
|
|
147
|
+
Store wraps the browser's `localStorage` API with:
|
|
148
|
+
|
|
149
|
+
- Automatic JSON serialization/deserialization
|
|
150
|
+
- Error handling for parse failures
|
|
151
|
+
- Size calculation
|
|
152
|
+
- Quota monitoring
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Storage Limits
|
|
157
|
+
|
|
158
|
+
- **Chrome/Safari**: ~5-10 MB
|
|
159
|
+
- **Firefox**: ~10 MB
|
|
160
|
+
- **Edge**: ~5-10 MB
|
|
161
|
+
|
|
162
|
+
Use `store.quota()` to monitor usage.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Best Practices
|
|
167
|
+
|
|
168
|
+
1. Prefix keys: `store.set('app_username', '...')`
|
|
169
|
+
2. Check before set: `if (store.get('key')) { ... }`
|
|
170
|
+
3. Handle quota: Try/catch around large data
|
|
171
|
+
4. Clean up: `store.removeAll()` on logout
|
package/markdown/SYNC.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
> **Status:** Published
|
|
2
|
+
> **Date:** 2026-09-12
|
|
3
|
+
> **Deciders:** Memorio 5.x Core Team
|
|
4
|
+
> **Scope**: API Reference
|
|
5
|
+
> **Standard**: Memorio API Specification v5
|
|
6
|
+
>
|
|
7
|
+
---
|
|
8
|
+
# Synchronization & Cloud (optional)
|
|
9
|
+
|
|
10
|
+
`memorio.memory` is **local-first**. Data is created and served from the device;
|
|
11
|
+
the cloud is only ever a **transport/persistence provider**, never the source of
|
|
12
|
+
truth. Enabling sync does not replace local storage - it *mirrors* it.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
memorio
|
|
16
|
+
│
|
|
17
|
+
┌────────┴────────┐
|
|
18
|
+
│ Memory Engine │
|
|
19
|
+
└────────┬────────┘
|
|
20
|
+
┌────────────┼────────────┐
|
|
21
|
+
▼ ▼ ▼
|
|
22
|
+
local SQLite cloud
|
|
23
|
+
memory durable sync
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 1. The rule: the data is born local
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
memorio.memory.remember('user.language', 'Italian', { scope: 'local' })
|
|
30
|
+
// ↓ local first
|
|
31
|
+
// store / sessionStorage / IndexedDB / sql.js
|
|
32
|
+
// ↓ sync / push (when online)
|
|
33
|
+
// cloud provider
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The cloud therefore does not **replace** memory: it **replicates** it. This
|
|
37
|
+
gives you: offline-first, lowest latency, data available immediately,
|
|
38
|
+
synchronization when online, multi-device, multi-user, centralized persistence.
|
|
39
|
+
|
|
40
|
+
We deliberately do **not** provide:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// ❌ two mental models
|
|
44
|
+
memory.cloud.save(...)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Instead:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
memorio.memory.remember('user.language', 'Italian')
|
|
51
|
+
// and a single configuration point:
|
|
52
|
+
memorio.memory.configure({ sync: { provider: myCloudProvider, namespace: '…' } })
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 2. Scopes (isolation, not a security boundary)
|
|
56
|
+
|
|
57
|
+
| Scope | Lifetime | Syncs by default |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `'device'` | this browser/device only | no (sticky) |
|
|
60
|
+
| `'user'` | follows the user across devices | yes (requires provider + namespace) |
|
|
61
|
+
| `'shared'` | shared across users / tenant | yes (requires provider + namespace) |
|
|
62
|
+
|
|
63
|
+
> As with `memorio.createContext`, **scoping is a naming convention, not a
|
|
64
|
+
> security boundary.** Enforce real isolation server-side.
|
|
65
|
+
|
|
66
|
+
## 3. SQLite as the local durable store
|
|
67
|
+
|
|
68
|
+
SQLite (`sql.js`) is **in-memory by default** (volatie per page load). It becomes
|
|
69
|
+
the durable journal/value store when you opt in:
|
|
70
|
+
|
|
71
|
+
- `sqlite.config({ persistence: true })` / `sqlite.db.create('app', { persistence: true })`
|
|
72
|
+
snapshot the database to `store` (localStorage) and restore it on reopen.
|
|
73
|
+
- Writes are snapshotted via sql.js `updateHook` (debounced).
|
|
74
|
+
- `sqlite.db.persist(name)` forces an immediate save; `sqlite.db.close(name)`
|
|
75
|
+
flushes + closes; `sqlite.db.download(name, file?)` triggers a browser
|
|
76
|
+
`.sqlite` download (dev convenience).
|
|
77
|
+
|
|
78
|
+
See `docs/markdown/SQLITE.md` for the full SQLite reference.
|
|
79
|
+
|
|
80
|
+
## 4. The local operation journal
|
|
81
|
+
|
|
82
|
+
The **sync journal** is the durable op log that drives cloud reconciliation.
|
|
83
|
+
It is persisted on `store` (localStorage) - **not** on an in-memory sql.js db,
|
|
84
|
+
because pending operations must survive a refresh for offline-first to work.
|
|
85
|
+
|
|
86
|
+
| Method | Returns | Notes |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `memory.journal.append(entry, operation)` | `Promise<MemoryEntry>` | records `remember\|update\|forget\|expire\|confirm\|supersede` with `sync:'pending'` |
|
|
89
|
+
| `memory.journal.pending()` | `Promise<MemoryEntry[]>` | rows where `sync != 'synced'`, for the current namespace |
|
|
90
|
+
| `memory.journal.markSynced(ids)` | `Promise<number>` | advances rows to `synced` (namespace-scoped) |
|
|
91
|
+
| `memory.journal.get(id)` | `Promise<MemoryEntry \| null>` | single entry, namespace-scoped |
|
|
92
|
+
| `memory.journal.clear()` | `Promise<void>` | wipes the current namespace's journal |
|
|
93
|
+
| `memory.journal.replay()` | `Promise<SyncAck>` | pushes `pending()` to the provider, marks synced, optional `pull` |
|
|
94
|
+
| `memory.journal.status()` | `Promise<'store'>` | the substrate in use |
|
|
95
|
+
|
|
96
|
+
We sync **operations of memory**, never a raw database dump:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
user A device A
|
|
100
|
+
remember X ─────► local ─────► sync ─────► cloud
|
|
101
|
+
forget Z ──────► local ─────► sync ─────► cloud
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 5. Conflict resolution
|
|
105
|
+
|
|
106
|
+
The cloud must not simply say "last write wins." Memorio tags every entry with:
|
|
107
|
+
|
|
108
|
+
- `confidence` (0–1, user/system trust in the value)
|
|
109
|
+
- `lastConfirmedAt` / `updatedAt` (epoch ms)
|
|
110
|
+
- `version` (monotonic per-key counter)
|
|
111
|
+
- `source` / `scope`
|
|
112
|
+
|
|
113
|
+
Remote conflicts are surfaced as `sync:'conflict'` rows via
|
|
114
|
+
`journal.pending()`; the provider's `resolve(op)` hint decides locally. Example:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
Laptop: language=Italian, confidence=0.92
|
|
118
|
+
Phone: language=English, confidence=0.61
|
|
119
|
+
→ higher-confidence entry wins locally; the provider decides for shared scope.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## 6. Configuring a backend
|
|
123
|
+
|
|
124
|
+
Sync is **opt-in**. You supply an application-owned `provider` that knows how to
|
|
125
|
+
talk to your backend (REST, WebSocket, Supabase, a custom agent server, …).
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
memorio.memory.configure({
|
|
129
|
+
namespace: 'user:123:device:abc', // tenant/user/device - partitions the journal
|
|
130
|
+
provider: {
|
|
131
|
+
push(ops) { return fetch('/api/sync', { method: 'POST', body: JSON.stringify(ops), headers: authHeaders }) }
|
|
132
|
+
pull(since) { return fetch(`/api/sync?since=${since}`).then(r => r.json()) }
|
|
133
|
+
resolve(op) { return op.confidence >= 0.8 ? 'local' : 'remote' }
|
|
134
|
+
},
|
|
135
|
+
auto: true // auto-replay on focus/online (default true)
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
interface SyncProvider {
|
|
141
|
+
push(ops: MemoryEntry[]): Promise<{ synced: string[]; conflicts?: string[]; error?: string }>
|
|
142
|
+
pull?(since?: number): Promise<MemoryEntry[]>
|
|
143
|
+
resolve?(op: MemoryEntry): Promise<'local' | 'remote' | 'merge'>
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`memorio.memory.ready` resolves once the local journal substrate is chosen.
|
|
148
|
+
|
|
149
|
+
## 7. Security (NIST / OWASP / NSA posture)
|
|
150
|
+
|
|
151
|
+
- **Memorio never handles credentials.** No passwords, tokens, or API keys are
|
|
152
|
+
read from or stored by memorio. Authentication/authorization live in your
|
|
153
|
+
`provider`/backend (OWASP A01: Broken Access Control).
|
|
154
|
+
- **Namespace isolation.** The journal is keyed by `namespace:id` at the storage
|
|
155
|
+
layer; there is **no API** to enumerate or open another namespace's journal. A
|
|
156
|
+
client holding a forged/fake namespace simply sees its own (empty) journal.
|
|
157
|
+
- **No dynamic code.** Journal entries are strictly JSON-round-tripped,
|
|
158
|
+
size-capped (10 MB/entry), and never `eval`'d. The sql.js loader never
|
|
159
|
+
`import()`s a bare specifier that could be hijacked at build time.
|
|
160
|
+
- **Trust boundary:** memorio owns the local durable copy + operation log; the
|
|
161
|
+
provider/backend owns remote-side auth and conflict resolution. Memorio
|
|
162
|
+
surfaces `conflict`/`error` rows; it does not fabricate a winner.
|
|
163
|
+
- **Data-at-rest (NSA/CISA).** memorio's `store`/`idb`/`sqlite` snapshots are
|
|
164
|
+
**not encrypted**. If you persist user data server-side or ship it through your
|
|
165
|
+
backend, encrypt it server-side with keys you manage - memorio treats the local
|
|
166
|
+
store as untrusted-from-the-browser and does not attest its own integrity.
|
|
167
|
+
|
|
168
|
+
## 8. Where data lives
|
|
169
|
+
|
|
170
|
+
| Substrate | API | Volatile? | Persistent? |
|
|
171
|
+
|---|---|---|---|
|
|
172
|
+
| in-memory `Proxy` | `state` | yes (per tab) | no |
|
|
173
|
+
| `localStorage` / Map | `store` | no | yes (browser) |
|
|
174
|
+
| `sessionStorage` / Map | `session` | no | per-tab (browser) |
|
|
175
|
+
| IndexedDB | `idb`, `memory` durable | no | yes |
|
|
176
|
+
| sql.js (WASM heap) | `sqlite` | **yes** | only with `persistence: true` (snapshot → `store`) |
|
|
177
|
+
| sync journal | `memory.journal` | no | yes (`store`) |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 9. Evolving the journal toward multi-device consistency
|
|
182
|
+
|
|
183
|
+
Moving from sequential, single-device sync to concurrent offline edits across
|
|
184
|
+
multiple devices is a classic local-first challenge. Below are six
|
|
185
|
+
architectural strategies, in increasing order of sophistication, that can be
|
|
186
|
+
layered onto the existing journal **without** adopting a full CRDT framework
|
|
187
|
+
(Yjs, Automerge, etc.).
|
|
188
|
+
|
|
189
|
+
> **Scope note.** These strategies target `memorio.memory` first - it already
|
|
190
|
+
> carries the metadata a journal needs (`confidence`, `source`, `tag`, `scope`,
|
|
191
|
+
> `createdAt`, `lastConfirmedAt`). If synchronization is ever extended to
|
|
192
|
+
> `state` or `store`, those layers must gain HLC timestamps and path-level
|
|
193
|
+
> fields explicitly - they cannot inherit them from `memory`.
|
|
194
|
+
|
|
195
|
+
### 9.1 Field-level / path-level journaling
|
|
196
|
+
|
|
197
|
+
Recording an entire entity in the journal makes orthogonal edits un-mergeable:
|
|
198
|
+
|
|
199
|
+
```json
|
|
200
|
+
{ "op": "set", "path": "user.role", "value": "admin", "timestamp": 1710000000 }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Device A writes `user.name`, device B writes `user.role` - both are patch
|
|
204
|
+
operations on the **same entity**. An entity-level journal would produce one
|
|
205
|
+
opaque `UPDATE user = {…}` and one write would clobber the other. Path-level
|
|
206
|
+
patches merge automatically because the paths are disjoint.
|
|
207
|
+
|
|
208
|
+
> **Not a panacea.** Path-level journaling merges edits to *different* fields.
|
|
209
|
+
> Two devices writing the **same** path concurrently still need explicit conflict
|
|
210
|
+
> resolution (Section 5). HLC tells you *when* the events happened; it does not
|
|
211
|
+
> tell you *which value wins* when events are truly concurrent on the same path.
|
|
212
|
+
|
|
213
|
+
### 9.2 Causal ordering with Hybrid Logical Clocks (HLC)
|
|
214
|
+
|
|
215
|
+
Wall-clock timestamps alone fail under clock drift. Associate every journal
|
|
216
|
+
entry with an HLC that combines a physical component, a logical counter, and a
|
|
217
|
+
node identifier:
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
hlc:1710000005:2:deviceB
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
This gives constant-size causal ordering (vs. vector clocks, which grow with
|
|
224
|
+
the number of writers - problematic for a bounded journal). The sync engine can
|
|
225
|
+
then apply causally-dependent operations in order and invoke the conflict
|
|
226
|
+
resolver only for genuinely concurrent writes on the same path.
|
|
227
|
+
|
|
228
|
+
### 9.3 Ordering collections without full OT - fractional indexing
|
|
229
|
+
|
|
230
|
+
Arrays and ordered lists are the hardest non-CRDT case. Numeric indices shift
|
|
231
|
+
when a peer inserts or deletes nearby. Two lightweight options:
|
|
232
|
+
|
|
233
|
+
1. **Keyed collections** - treat list items as a `Map<id, value>` rather than a
|
|
234
|
+
positional array. No index renumbering needed.
|
|
235
|
+
2. **Fractional indexing** - assign each element a sortable key between its
|
|
236
|
+
neighbours (e.g. `1.0`, `2.0` → insert at `1.5`). On repeated re-inserts
|
|
237
|
+
between the same pair, keys grow in length and should be rebalanced
|
|
238
|
+
periodically. Use a mature library (`fractional-indexing` on npm) rather than
|
|
239
|
+
reimplementing the arithmetic.
|
|
240
|
+
|
|
241
|
+
### 9.4 Explicit deletions (tombstones)
|
|
242
|
+
|
|
243
|
+
A bare "remove" entry can be resurrected as a "zombie" when a concurrent
|
|
244
|
+
update is replayed after it. Instead, record every `forget` / `delete` as a
|
|
245
|
+
first-class journal event with its own HLC:
|
|
246
|
+
|
|
247
|
+
```json
|
|
248
|
+
{ "op": "delete", "path": "user.role", "timestamp": "hlc:1710000005:0:deviceA" }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The tombstone participates in the same causal comparison as `set` events: if
|
|
252
|
+
the delete's HLC succeeds the update's, the field is gone; if it precedes, the
|
|
253
|
+
update is re-applied. Keep tombstones around until all known peers have
|
|
254
|
+
acknowledged them, then garbage-collect during a maintenance sweep.
|
|
255
|
+
|
|
256
|
+
### 9.5 Hybrid sync - server-assisted consensus
|
|
257
|
+
|
|
258
|
+
Since memorio's cloud role is transport-and-acknowledgement (not source of
|
|
259
|
+
truth), the backend can resolve conflicts on the server and return the
|
|
260
|
+
canonical sequence:
|
|
261
|
+
|
|
262
|
+
| Pattern | Description |
|
|
263
|
+
|---|---|
|
|
264
|
+
| **Optimistic local apply** | Apply the local journal entry immediately and emit reactive events. |
|
|
265
|
+
| **Server ack** | Backend validates causal order against the central state and returns the official sequence. |
|
|
266
|
+
| **Client journal rebase** | Confirmed entries are purged from the local journal; unconfirmed local entries are replayed on top of the acknowledged state. |
|
|
267
|
+
|
|
268
|
+
### 9.6 Validation - convergence simulation
|
|
269
|
+
|
|
270
|
+
Causal correctness is theoretical until you test it across replay orderings:
|
|
271
|
+
|
|
272
|
+
- Generate random concurrent `set` / `delete` operations across N simulated
|
|
273
|
+
devices (shared paths and disjoint paths).
|
|
274
|
+
- Replay the operation log in every plausible ordering on each simulated
|
|
275
|
+
device.
|
|
276
|
+
- Assert **state convergence**: every device arrives at the same final state
|
|
277
|
+
regardless of delivery order.
|
|
278
|
+
- Include hand-crafted pathological cases (`update` vs concurrent `delete` on
|
|
279
|
+
the same path, `insert` vs concurrent `delete` on the same array index,
|
|
280
|
+
interleaved reorders).
|
|
281
|
+
|
|
282
|
+
### 9.7 Migration
|
|
283
|
+
|
|
284
|
+
Path-level journal entries with HLC and tombstones are a format change from
|
|
285
|
+
entity-level entries. Mitigate with:
|
|
286
|
+
|
|
287
|
+
- An explicit **version header** on every journal entry.
|
|
288
|
+
- A clear migration policy: either a one-time compaction pass that folds
|
|
289
|
+
legacy entity-level entries into the current state and starts a fresh
|
|
290
|
+
journal, or a dual-format reader that can replay both formats during the
|
|
291
|
+
transition window.
|
|
292
|
+
|
|
293
|
+
### 9.8 Recommended architecture diagram
|
|
294
|
+
|
|
295
|
+
```text
|
|
296
|
+
[ Local Mutation ]
|
|
297
|
+
│
|
|
298
|
+
▼
|
|
299
|
+
[ Field-Level Patch (set/delete) + HLC Timestamp ]
|
|
300
|
+
│
|
|
301
|
+
├───► Local State (immediate reactive update)
|
|
302
|
+
│
|
|
303
|
+
└───► Local Journal (incl. tombstones for deletes)
|
|
304
|
+
│
|
|
305
|
+
(Online Sync)
|
|
306
|
+
│
|
|
307
|
+
▼
|
|
308
|
+
[ Backend Conflict Resolver ]
|
|
309
|
+
(concurrent path → resolveConflict;
|
|
310
|
+
otherwise apply in HLC order)
|
|
311
|
+
│
|
|
312
|
+
▼
|
|
313
|
+
[ State Ack / Rebased Journal ]
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Applying granular path-level patches, HLC for causality, fractional indexing
|
|
317
|
+
for ordered collections, explicit tombstones for deletions, and a convergence
|
|
318
|
+
test suite lets memorio handle high-frequency concurrent offline edits across
|
|
319
|
+
devices without the overhead of a full CRDT stack.
|