memorio 5.1.2 → 5.1.4

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/llms.txt CHANGED
@@ -21,8 +21,8 @@ Memorio provides 6 storage modules plus utilities:
21
21
  | `session` | sessionStorage | Dies with browser tab; falls back to non-durable in-memory `Map` in Node.js/Deno |
22
22
  | `cache` | In-memory cache | Fastest read, no persistence |
23
23
  | `idb` | IndexedDB | Structured, async, persistent (browser-only - disabled in Node.js/Deno) |
24
- | `sqlite` | SQLite via sql.js (WebAssembly) | Browser-only relational SQL |
25
- | `observer` | Object watcher | Legacy; string-based paths, not statically checked against `state`'s shape |
24
+ | `sqlite` | SQLite via sql.js (WebAssembly) / `bun:sqlite` (native) | Browser-only relational SQL; optional journal + checkpoint persistence to `store` |
25
+ | `observer` | Object watcher | String-based path observer; use `typed<T>()` + `registerSchema()` for statically checked alternatives |
26
26
  | `useObserver` | React hook | Auto-discovery of state paths |
27
27
  | `encryption` | AES-GCM + PBKDF2 | Encrypt/decrypt values, store/session integration |
28
28
 
@@ -51,6 +51,8 @@ To opt in to global access (state, store, session, cache, idb, sqlite, observer,
51
51
  import 'memorio/global'
52
52
  ```
53
53
 
54
+ **Upgrading from 4.x:** in v5, `import 'memorio'` no longer installs globals on `globalThis`. If your code uses bare globals (e.g. `state.counter = 1` without importing `state`), change the import to `import 'memorio/global'`. Named imports (`import { state } from 'memorio'`) are unaffected. After upgrading, restart your dev server and clear `node_modules/.vite` / `.next/cache` to avoid stale pre-bundles masking the change.
55
+
54
56
  This is independent of bundler environment detection (no `import.meta.env.DEV`, no `process.env.NODE_ENV` checks).
55
57
 
56
58
  ## API Reference
@@ -89,6 +91,19 @@ These are materially different behaviors - confirm against the actual source whi
89
91
  - Automatic path tracking via `__path` property
90
92
  - Nested proxy support
91
93
  - Auto-dispatches events on changes
94
+ - `state.persist(path)` mirrors state path to `store` and syncs writes (see "Getting Started" guide)
95
+
96
+ #### `state.persist(path)`
97
+
98
+ Creates a bidirectional mirror between `state` (live in-memory) and `store` (durable copy). Seeds the initial value from `store` if present, then every write to that path flows to `store` automatically.
99
+
100
+ ```javascript
101
+ const stopPersisting = state.persist('state.user')
102
+ state.user = { name: 'Sara' }
103
+ // ... writes to state.user are mirrored to store
104
+
105
+ stopPersisting() // stops mirroring; store value is preserved
106
+ ```
92
107
 
93
108
  ### `store` - localStorage Persistence
94
109
 
@@ -69,6 +69,33 @@ Permanently removes a memory.
69
69
  await memorio.memory.forget('user.temp')
70
70
  ```
71
71
 
72
+ ### `memorio.memory.patch(key, patches)`
73
+
74
+ Apply a field-level JSON Patch to an existing memory entry's value. Supports `set` and `delete` operations.
75
+
76
+ ```ts
77
+ await memorio.memory.remember('user', {
78
+ name: 'Mario',
79
+ profile: { theme: 'light', lang: 'en' }
80
+ })
81
+
82
+ // Set a nested value
83
+ await memorio.memory.patch('user', [
84
+ { op: 'set', path: 'user.profile.theme', value: 'dark' }
85
+ ])
86
+
87
+ // Delete a field
88
+ await memorio.memory.patch('user', [
89
+ { op: 'delete', path: 'user.profile.lang' }
90
+ ])
91
+ ```
92
+
93
+ | Field | Type | Description |
94
+ |-------|------|-------------|
95
+ | `op` | `'set' \| 'delete'` | The patch operation type |
96
+ | `path` | `string` | Dot-notation path relative to the entry value (e.g. `'user.profile.theme'`) |
97
+ | `value` | `any` | The value to set (only for `op: 'set'`) |
98
+
72
99
  ### `memorio.memory.context(opts?)`
73
100
 
74
101
  Returns ranked, relevant memories. Uses recency × confidence × type/scope boost algorithm.
@@ -1,10 +1,10 @@
1
- > **Status:** Published
2
- > **Date:** 2026-09-12
3
- > **Scope**: API Reference
4
- > **Standard**: Memorio API Specification v5
5
- >
6
- ---
7
- # SQLite - Memorio
1
+ > **Status:** Published
2
+ > **Date:** 2026-09-12
3
+ > **Scope**: API Reference
4
+ > **Standard**: Memorio API Specification v5
5
+ >
6
+ ---
7
+ # SQLite - Memorio
8
8
 
9
9
  > The `sql.js` package is **optional** - if it is not installed, Memorio loads `sql.js` on first use by injecting the `sql-wasm-browser.js` UMD build from the jsDelivr CDN as a classic `<script>` (which exposes the `initSqlJs` factory on `globalThis`). No bare `import('sql.js')` is ever emitted, so bundlers never resolve the optional dependency at build time. Not available in Node.js/Deno.
10
10
 
@@ -97,16 +97,16 @@ console.debug('SQLite version:', sqlite.db.version());
97
97
  ### Example 5: Persistence & dev download
98
98
 
99
99
  > **Important:** sql.js databases live in **WebAssembly memory** - they are
100
- > in-memory and **volatile** (lost on page refresh) unless you persist them.
100
+ > in-memory and **volatile** (lost on page refresh) unless you opt into persistence.
101
101
 
102
102
  ```javascript
103
- // Opt into auto-persistence at create time (global config or per-create)
103
+ // Opt into persistence at create time (global config or per-create)
104
104
  await sqlite.db.create('app', { persistence: true });
105
105
 
106
106
  await sqlite.query.run('app', 'CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)');
107
- // writes are snapshotted to localStorage automatically (debounced via updateHook)
107
+ // writes are journaled incrementally; checkpoints are saved automatically
108
108
 
109
- await sqlite.db.persist('app'); // force an immediate snapshot
109
+ await sqlite.db.persist('app'); // force an immediate checkpoint (full snapshot + clear journal)
110
110
  // After a refresh, reopening restores the data:
111
111
  await sqlite.db.create('app', { persistence: true });
112
112
 
@@ -117,10 +117,33 @@ await sqlite.db.download('app', 'app.sqlite');
117
117
  await sqlite.db.close('app');
118
118
  ```
119
119
 
120
+ Persistence uses a **journal + checkpoint** strategy for incremental durability:
121
+
122
+ 1. **Journal**: every `db.run(sql, params)` write is recorded as a journal entry
123
+ (with an HLC timestamp for causal ordering) in `store` (localStorage). The
124
+ journal is append-only and incremental - only the changed statement is stored.
125
+ 2. **Checkpoint**: every 50 operations (configurable), or after 250 ms of write
126
+ idle, a full database snapshot is taken and written to `store`. The journal is
127
+ then cleared up to that point.
128
+ 3. **Recovery**: on `create(name, { persistence: true })`, the latest checkpoint
129
+ is loaded first, then any journal entries that were written after the
130
+ checkpoint are replayed on top - so no writes are lost even if the page closes
131
+ between checkpoints.
132
+
120
133
  Persistence is stored on `store` (localStorage, namespaced per
121
- `sqlite.config({ namespace })` / memorio context) as a base64 snapshot under
122
- `memorio:sqlite:db:<ns>:<name>`. It is best-effort: if `store` is unavailable
123
- the database simply behaves as in-memory.
134
+ `sqlite.config({ namespace })` / memorio context) as:
135
+ - Checkpoint: `memorio:sqlite:db:<ns>:<name>` (base64 binary snapshot)
136
+ - Journal entries: `memorio:sqlite:journal:<ns>:<name>:<seq>` (individual SQL ops)
137
+
138
+ It is best-effort: if `store` is unavailable the database simply behaves as
139
+ in-memory. The journal is also available directly:
140
+
141
+ ```javascript
142
+ sqlite.db.persist('app'); // force checkpoint (alias: flush)
143
+ sqlite.db.flush('app'); // force checkpoint
144
+ const entries = sqlite.db.journal.entries('app'); // inspect pending ops
145
+ await sqlite.db.journal.checkpoint('app'); // manual checkpoint
146
+ ```
124
147
 
125
148
  ---
126
149
 
@@ -130,11 +153,13 @@ the database simply behaves as in-memory.
130
153
 
131
154
  | Method | Parameters | Returns | Description |
132
155
  |--------|------------|---------|-------------|
133
- | `sqlite.config(opts)` | `opts: { loader?, locateFile?, wasmUrl?, persistence?, namespace? }` | `sqlite` | Configure the engine. `loader` fully replaces the initializer (default = CDN `<script>`); `locateFile` customizes wasm resolution; `wasmUrl` sets the wasm base directory; `persistence` toggles automatic db snapshots; `namespace` partitions persisted snapshots. Chainable. |
134
- | `sqlite.db.create(name, opts)` | `name: string, opts?: { data?, persistence? }` | `Database` | Opens/creates a named in-memory db. With `persistence: true` (or global config), a snapshot is restored if present and writes are auto-saved. |
135
- | `sqlite.db.persist(name)` | `name: string` | `Promise<boolean>` | Force an immediate snapshot of a persisted database to `store`. |
156
+ | `sqlite.config(opts)` | `opts: { loader?, locateFile?, wasmUrl?, persistence?, namespace? }` | `sqlite` | Configure the engine. `loader` fully replaces the initializer (default = CDN `<script>`); `locateFile` customizes wasm resolution; `wasmUrl` sets the wasm base directory; `persistence` toggles journal + checkpoint persistence; `namespace` partitions persisted data. Chainable. |
157
+ | `sqlite.db.create(name, opts)` | `name: string, opts?: { data?, persistence? }` | `Database` | Opens/creates a named in-memory db. With `persistence: true` (or global config), the latest checkpoint is restored and pending journal entries are replayed for full crash recovery. |
158
+ | `sqlite.db.persist(name)` | `name: string` | `Promise<boolean>` | Force an immediate checkpoint (full snapshot + journal clear) of a persisted database to `store`. Alias of `flush`. |
159
+ | `sqlite.db.flush(name)` | `name: string` | `Promise<boolean>` | Force an immediate checkpoint (alias of `persist`). |
136
160
  | `sqlite.db.download(name, filename?)` | `name: string, filename?: string` | `Promise<boolean>` | Dev-only: trigger a browser download of the db as a `.sqlite` file. |
137
- | `sqlite.db.close(name)` | `name: string` | `Promise<void>` | Persist (if enabled), close, and release the handle. |
161
+ | `sqlite.db.close(name)` | `name: string` | `Promise<void>` | Checkpoint (if enabled), close, and release the handle. |
162
+ | `sqlite.db.journal` | `name: string` | Object | Journal namespace: `append`, `entries`, `count`, `clear`, `replay`, `checkpoint`. |
138
163
  | `sqlite.ready` | none | `Promise<void>` | Resolves once `sql.js` has been initialized. Rejects (and sets `_disabled`) if the engine can't load. |
139
164
  | `sqlite.db.version()` | none | `string \| null` | Engine version, available after initialization. |
140
165
  | `sqlite._disabled` | none | `boolean` | `true` when the module is disabled (non-browser env / load failure). |
@@ -147,7 +172,7 @@ the database simply behaves as in-memory.
147
172
  | `sqlite.db.support()` | none | `boolean` | Check whether SQLite can run in this environment. |
148
173
  | `sqlite.db.create(name, opts?)` | `name: string`, `opts?: { data }` | `Promise<Database>` | Create an in-memory database (or reopen one from a binary dump). |
149
174
  | `sqlite.db.get(name)` | `name: string` | `Database` | Retrieve an open database handle. |
150
- | `sqlite.db.delete(name)` | `name: string` | `boolean` | Close and remove a database handle. |
175
+ | `sqlite.db.delete(name)` | `name: string` | `Promise<boolean>` | Checkpoint (if enabled), close, and remove a database handle. |
151
176
  | `sqlite.db.list()` | none | `string[]` | List open database names. |
152
177
  | `sqlite.db.size(name?)` | `name?: string` | `Promise<number>` | Size in bytes of one or all databases. |
153
178
  | `sqlite.db.export(name)` | `name: string` | `Promise<Uint8Array>` | Export a database to a binary dump. |
@@ -184,6 +209,7 @@ the database simply behaves as in-memory.
184
209
 
185
210
  1. Configure the wasm path for production: `sqlite.config({ wasmUrl })` or `sqlite.config({ loader })`.
186
211
  2. Create one named database per feature and reuse the handle: `const db = await sqlite.db.create('app')`.
187
- 3. Always release databases you no longer need: `sqlite.db.delete('temp')`.
188
- 4. Use `await sqlite.ready` before running statements to ensure the engine is loaded.
189
- 5. Use `?` placeholders and `params` to avoid SQL injection.
212
+ 3. Use `await sqlite.ready` before running statements to ensure the engine is loaded.
213
+ 4. Use `?` placeholders and `params` to avoid SQL injection.
214
+ 5. Batch writes and call `sqlite.db.persist()` / `sqlite.db.flush()` once after the batch, not inside a loop. The journal captures each write incrementally, but the checkpoint (full snapshot) is the expensive operation.
215
+ 6. Always release databases you no longer need: `await sqlite.db.delete('temp')`.
package/markdown/STORE.md CHANGED
@@ -1,10 +1,10 @@
1
- > **Status:** Published
2
- > **Date:** 2026-09-12
3
- > **Scope**: API Reference
4
- > **Standard**: Memorio API Specification v5
5
- >
6
- ---
7
- # Store - Memorio
1
+ > **Status:** Published
2
+ > **Date:** 2026-09-12
3
+ > **Scope**: API Reference
4
+ > **Standard**: Memorio API Specification v5
5
+ >
6
+ ---
7
+ # Store - Memorio
8
8
 
9
9
  > 🖥️ **Browser & Edge**: Uses localStorage for persistence
10
10
  > ⚙️ **Node.js/Deno**: Falls back to in-memory storage (not persistent)
@@ -56,7 +56,7 @@ store.remove('username');
56
56
 
57
57
  // Check size
58
58
  const totalSize = store.size();
59
- console.debug(`${totalSize} bytes`);
59
+ console.debug(`${totalSize} KB`);
60
60
  ```
61
61
 
62
62
  ### Example 3: Advanced
@@ -66,9 +66,9 @@ console.debug(`${totalSize} bytes`);
66
66
  const [used, total] = await store.quota();
67
67
  console.debug(`Using ${used} out of ${total} KB`);
68
68
 
69
- // Get total size in characters
69
+ // Get total size in kilobytes
70
70
  const size = store.size();
71
- console.debug(`${size} bytes`);
71
+ console.debug(`${size} KB`);
72
72
 
73
73
  // Clear all data
74
74
  store.removeAll();
@@ -97,7 +97,7 @@ try {
97
97
  | `store.delete(name)` | `name: string` | `boolean` | Alias for remove |
98
98
  | `store.removeAll()` | none | `boolean` | Clear all storage |
99
99
  | `store.clearAll()` | none | `boolean` | Alias for removeAll |
100
- | `store.size()` | none | `number` | Get total size in characters |
100
+ | `store.size()` | none | `number` | Get total size in kilobytes (KB) |
101
101
  | `store.quota()` | none | `Promise<[number, number]>` | Get storage usage/quota in KB |
102
102
 
103
103
  ### Properties
@@ -167,4 +167,4 @@ Use `store.quota()` to monitor usage.
167
167
  1. Prefix keys: `store.set('app_username', '...')`
168
168
  2. Check before set: `if (store.get('key')) { ... }`
169
169
  3. Handle quota: Try/catch around large data
170
- 4. Clean up: `store.removeAll()` on logout
170
+ 4. Clean up: `store.removeAll()` on logout
package/markdown/SYNC.md CHANGED
@@ -1,10 +1,10 @@
1
- > **Status:** Published
2
- > **Date:** 2026-09-12
3
- > **Scope**: API Reference
4
- > **Standard**: Memorio API Specification v5
5
- >
6
- ---
7
- # Synchronization & Cloud (optional)
1
+ > **Status:** Published
2
+ > **Date:** 2026-09-12
3
+ > **Scope**: API Reference
4
+ > **Standard**: Memorio API Specification v5
5
+ >
6
+ ---
7
+ # Synchronization & Cloud (optional)
8
8
 
9
9
  `memorio.memory` is **local-first**. Data is created and served from the device;
10
10
  the cloud is only ever a **transport/persistence provider**, never the source of
@@ -67,12 +67,15 @@ memorio.memory.configure({ sync: { provider: myCloudProvider, namespace: '…' }
67
67
  SQLite (`sql.js`) is **in-memory by default** (volatie per page load). It becomes
68
68
  the durable journal/value store when you opt in:
69
69
 
70
- - `sqlite.config({ persistence: true })` / `sqlite.db.create('app', { persistence: true })`
71
- snapshot the database to `store` (localStorage) and restore it on reopen.
72
- - Writes are snapshotted via sql.js `updateHook` (debounced).
73
- - `sqlite.db.persist(name)` forces an immediate save; `sqlite.db.close(name)`
74
- flushes + closes; `sqlite.db.download(name, file?)` triggers a browser
75
- `.sqlite` download (dev convenience).
70
+ - `sqlite.config({ persistence: true })` / `sqlite.db.create('app', { persistence: true })`
71
+ snapshot the database to `store` (localStorage) and restore it on reopen.
72
+ - Writes are journaled incrementally (append-only, HLC-timestamped) and
73
+ checkpointed periodically (every 50 ops or 250 ms of idle). On reopen, the
74
+ last checkpoint is loaded and the journal is replayed - no writes are lost.
75
+ - `sqlite.db.persist(name)` / `sqlite.db.flush(name)` forces an immediate
76
+ checkpoint; `sqlite.db.close(name)` checkpoints + closes;
77
+ `sqlite.db.download(name, file?)` triggers a browser `.sqlite` download
78
+ (dev convenience).
76
79
 
77
80
  See `docs/markdown/SQLITE.md` for the full SQLite reference.
78
81
 
@@ -172,7 +175,7 @@ interface SyncProvider {
172
175
  | `localStorage` / Map | `store` | no | yes (browser) |
173
176
  | `sessionStorage` / Map | `session` | no | per-tab (browser) |
174
177
  | IndexedDB | `idb`, `memory` durable | no | yes |
175
- | sql.js (WASM heap) | `sqlite` | **yes** | only with `persistence: true` (snapshot → `store`) |
178
+ | sql.js (WASM heap) | `sqlite` | **yes** | with `persistence: true` (journal + checkpoint → `store`) |
176
179
  | sync journal | `memory.journal` | no | yes (`store`) |
177
180
 
178
181
  ---
@@ -315,4 +318,4 @@ entity-level entries. Mitigate with:
315
318
  Applying granular path-level patches, HLC for causality, fractional indexing
316
319
  for ordered collections, explicit tombstones for deletions, and a convergence
317
320
  test suite lets memorio handle high-frequency concurrent offline edits across
318
- devices without the overhead of a full CRDT stack.
321
+ devices without the overhead of a full CRDT stack.
package/modules/redux.cjs CHANGED
@@ -471,8 +471,6 @@ var CONTEXTS_KEY, _contexts;
471
471
  var init_platform = __esm({
472
472
  "core/platform.ts"() {
473
473
  init_cjs_shims();
474
- init_internal();
475
- init_encryption();
476
474
  __name(getSessionStorage, "getSessionStorage");
477
475
  __name(getLocalStorage, "getLocalStorage");
478
476
  __name(hasIndexedDB, "hasIndexedDB");
@@ -1675,12 +1673,8 @@ function encodeHLC(hlcValue) {
1675
1673
  }
1676
1674
  __name(encodeHLC, "encodeHLC");
1677
1675
 
1678
- // core/mutation/patch.ts
1679
- init_cjs_shims();
1680
-
1681
1676
  // core/mutation/transaction.ts
1682
1677
  init_cjs_shims();
1683
- init_internal();
1684
1678
  var TX_REGISTRY_KEY = "__memorio_transaction_registry__";
1685
1679
  var registry = globalThis[TX_REGISTRY_KEY] || /* @__PURE__ */ new Map();
1686
1680
  if (!globalThis[TX_REGISTRY_KEY]) {
@@ -2035,19 +2029,39 @@ if (globalThis[STATE_INSTANCE_KEY]) {
2035
2029
  const initial = store.get(key);
2036
2030
  if (initial !== void 0 && initial !== null) {
2037
2031
  let acc = state2;
2032
+ let seedOk = true;
2038
2033
  for (let i = 0; i < segments.length - 1; i++) {
2039
2034
  const seg = segments[i];
2040
- if (acc[seg] === void 0 || acc[seg] === null) acc[seg] = {};
2041
- acc = acc[seg];
2035
+ const existing = acc[seg];
2036
+ if (existing === void 0 || existing === null) {
2037
+ acc[seg] = {};
2038
+ acc = acc[seg];
2039
+ } else if (typeof existing === "object") {
2040
+ acc = existing;
2041
+ } else {
2042
+ if (DEV) {
2043
+ console.warn(
2044
+ `[memorio state] state.persist('${normalized}') aborted seeding: '${seg}' already holds a primitive value (${typeof existing}), cannot nest '${segments.slice(i + 1).join(".")}' under it.`
2045
+ );
2046
+ }
2047
+ seedOk = false;
2048
+ break;
2049
+ }
2050
+ }
2051
+ if (seedOk && acc) {
2052
+ acc[segments[segments.length - 1]] = deepRaw(initial);
2042
2053
  }
2043
- if (acc) acc[segments[segments.length - 1]] = deepRaw(initial);
2044
2054
  }
2045
2055
  const handler = /* @__PURE__ */ __name(() => {
2046
2056
  let current = state2;
2047
2057
  for (const seg of segments) {
2048
2058
  current = current === void 0 || current === null ? void 0 : current[seg];
2049
2059
  }
2050
- store.set(key, deepRaw(current));
2060
+ if (current === void 0) {
2061
+ store.remove(key);
2062
+ } else {
2063
+ store.set(key, deepRaw(current));
2064
+ }
2051
2065
  }, "handler");
2052
2066
  const off = dispatch.listen(normalized, handler);
2053
2067
  return typeof off === "function" ? off : () => {
@@ -3037,19 +3051,22 @@ var memory = {
3037
3051
  async patch(key, patches) {
3038
3052
  const entry = await this._getEntry(key);
3039
3053
  if (!entry) return;
3054
+ const patchedValue = JSON.parse(JSON.stringify(entry.value ?? {}));
3040
3055
  let patched = false;
3041
3056
  for (const p of patches) {
3057
+ const relativePath = p.path.replace(/^[^.]+\./, "");
3042
3058
  if (p.op === "set") {
3043
- setNestedPath({ [entry.key]: entry.value }, p.path.replace(/^[^.]+\./, ""), p.value);
3059
+ setNestedPath(patchedValue, relativePath, p.value);
3044
3060
  patched = true;
3045
3061
  } else if (p.op === "delete") {
3046
- deleteNestedPath({ [entry.key]: entry.value }, p.path.replace(/^[^.]+\./, ""));
3062
+ deleteNestedPath(patchedValue, relativePath);
3047
3063
  patched = true;
3048
3064
  }
3049
3065
  }
3050
3066
  if (!patched) return;
3051
3067
  const updated = {
3052
3068
  ...entry,
3069
+ value: patchedValue,
3053
3070
  updatedAt: now(),
3054
3071
  lastConfirmedAt: now(),
3055
3072
  version: (entry.version ? entry.version : 0) + 1,