memorio 4.9.7 → 4.9.10

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 ADDED
@@ -0,0 +1,126 @@
1
+ # AGENTS.md — working with `memorio`
2
+
3
+ Operational conventions for AI coding agents working in a repository that depends on `memorio`.
4
+ See `README.md` and `llms.txt` for the full API. This file is decision rules: what to reach for,
5
+ when, and the specific footguns this library's own docs flag.
6
+
7
+ ## Read this before generating any `memorio` code
8
+
9
+ `memorio`'s own `llms.txt` contains an explicit unresolved discrepancy:
10
+
11
+ > **Locking — VERIFY BEFORE PUBLISHING:** one source describes per-key locking
12
+ > (`state.config.lock()` freezes only `config`); another describes global locking
13
+ > (`state.lock()` / `state.unlock()` freezing everything). Do not generate code against this
14
+ > section until it's resolved against the real source.
15
+
16
+ **Do not write or suggest `.lock()` / `.unlock()` calls with a confident claim about scope**
17
+ (per-key vs. global) until you've confirmed the actual signature against the installed
18
+ package's type definitions or source — `node_modules/memorio/dist/*.d.ts` — in the target repo.
19
+ If you can't confirm it, say so explicitly rather than picking one behavior and presenting it
20
+ as fact.
21
+
22
+ ## Choosing a layer
23
+
24
+ Don't default to `state` for everything just because it's the one-liner in the README. Walk the
25
+ decision tree from the README's ["Which layer should I use"](./README.md#which-layer-should-i-use)
26
+ section, and in particular:
27
+
28
+ | If the data is... | Use | Not |
29
+ | --- | --- | --- |
30
+ | UI-reactive, doesn't need to survive reload | `state` | `store` (unnecessary persistence) |
31
+ | Small, must survive reload, any tab | `store` | `idb` (overkill for simple k/v) |
32
+ | Must survive reload, this tab only | `session` | `store` (wrong scope) |
33
+ | Relational, needs joins/SQL | `sqlite` | `idb` (no query language) |
34
+ | Structured, needs confidence/TTL/source/history | `memory` | `state`/`store` (no lifecycle semantics) |
35
+ | Large or structured, browser-only | `idb` | `store` (size/shape limits) |
36
+
37
+ `sqlite` and `memory` answer different questions — "what data do I have" vs. "what does the app
38
+ believe, and how sure is it." Don't reach for `memory` just because a value happens to come from
39
+ an LLM; if it doesn't need confidence scoring, TTL, or supersession history, plain `state`/`store`
40
+ is simpler and correct.
41
+
42
+ ## Server-side and multi-request code — read this first
43
+
44
+ **`state` is a shared global namespace by default.** A value set in one request handler is
45
+ visible to every other read of `state` in the same process. There is no automatic per-request
46
+ or per-tab isolation.
47
+
48
+ Before generating any server-side code (API route, edge function, worker) that could handle more
49
+ than one user or request in the same process:
50
+
51
+ 1. Create an explicit context per request/tenant: `const ctx = memorio.createContext(id)`, then
52
+ read/write via `ctx.state`, never the bare global `state`.
53
+ 2. **Generate the context ID from trusted server-side data** (authenticated user id, request-scoped
54
+ token) — never directly from client-controlled input. Contexts are a key-prefix convention, not
55
+ a hard boundary, so a spoofable ID is a real collision risk, not just a naming concern.
56
+ 3. Treat context isolation as *organizational*, not a security boundary. It does not replace
57
+ authorization checks at the backend layer — never use it as the only thing standing between
58
+ one tenant's data and another's.
59
+
60
+ If the code you're generating is client-side (a single browser tab, one user), bare `state` is
61
+ fine — this concern is specific to shared-process server/edge code.
62
+
63
+ ## Known rough edges — don't paper over these in generated code
64
+
65
+ - **`store.quota()` returns `[0, 0]` for the `localStorage` backend** — it's a placeholder, not a
66
+ real reading. Don't generate capacity-check logic that branches on its result for `store`.
67
+ It may be meaningful for `idb`, but confirm before relying on it there too.
68
+ - **`idb` is disabled outright in Node.js/Deno** (calls warn and no-op). Any code that might run
69
+ outside a guaranteed browser context must guard: `if (idb.db.support())` or check
70
+ `memorio.getCapabilities().hasIndexedDB` — don't rely on the no-op warning alone reaching a log
71
+ anyone will see.
72
+ - **`sqlite` persistence serializes the entire database on every flush** — not incremental. Never
73
+ generate code that calls a persisting write inside a loop; batch writes in one transaction/run
74
+ and persist once after the batch, per the README's guidance.
75
+ - **`observer('state.some.path', cb)` paths are plain strings, unchecked against `state`'s actual
76
+ shape.** A typo or a later rename fails *silently* — the observer just never fires again. Prefer
77
+ `memorio.typed<T>()` + `registerSchema()` for anything you'd hate to have silently stop working,
78
+ and consider `memorio.pathExists('state.some.path')` as a startup sanity check for
79
+ observer-heavy code.
80
+ - **`memory.context()` is rule-based (tags/type/confidence/recency), not embedding-based.** Don't
81
+ imply semantic/meaning-based retrieval in comments or docs you generate — if the task genuinely
82
+ needs free-text similarity search, pair `memorio.memory` (for lifecycle: confidence, TTL,
83
+ supersession) with a separate embedding store, don't fake it with `context()` alone.
84
+ - **`memorio.logger` records the literal value written**, including tokens or PII, in
85
+ `getHistory()` / `exportLogs()`. Never enable it unconditionally on a production path that
86
+ touches sensitive data, and never wire `exportLogs()` output to analytics or error reporters
87
+ without redaction first.
88
+ - **Nothing in `store`, `session`, `idb`, or `memory` is encrypted.** Don't store auth tokens or
89
+ regulated data in any of these layers without adding your own encryption — flag it in code
90
+ review comments if you see it happening.
91
+
92
+ ## React usage
93
+
94
+ `useObserver` has two modes — pick deliberately, don't mix them by accident:
95
+
96
+ ```tsx
97
+ // Auto-discovery: re-runs when any state path touched during the callback changes
98
+ useObserver(() => { console.debug('counter:', state.counter) }, state.counter)
99
+
100
+ // Explicit deps: only re-runs when the listed paths change
101
+ useObserver(() => { console.debug('user:', state.user) }, [state.user])
102
+ ```
103
+
104
+ `useObserver` is a thin bridge onto `observer` — it inherits the "unchecked path string" caveat
105
+ above for any path passed in string form.
106
+
107
+ ## Cross-platform code
108
+
109
+ Before calling `idb`, `sqlite`, or `devtools`, or before relying on `store`/`session` durability,
110
+ check `memorio.getCapabilities()` rather than branching on `memorio.isNode()`/`isBrowser()` alone
111
+ — capabilities can differ inside a single platform category (e.g. some edge runtimes expose
112
+ `localStorage`, some don't). Generated code that assumes "browser == has IndexedDB" or
113
+ "Node == no persistence at all" will be wrong on edge runtimes.
114
+
115
+ ## Testing
116
+
117
+ When testing `memory` lifecycle behavior (supersession, TTL, confidence), assert on the *history*
118
+ being preserved, not just the current `recall()` value — `update()` is documented to supersede,
119
+ not overwrite, and a test that only checks the latest value won't catch a regression that
120
+ silently starts overwriting instead.
121
+
122
+ ## Related examples
123
+
124
+ See `examples/` in this repo for runnable reference implementations:
125
+ `multi-tenant-context.ts`, `react-observer.tsx`, `semantic-memory.ts`,
126
+ `sqlite-batched-writes.ts`, and `cross-platform-guards.ts`.
package/README.md CHANGED
@@ -285,6 +285,18 @@ memorio.registerSchema('user', {
285
285
  state.user = { name: 'Sara' } // rejected — missing required "email"
286
286
  ```
287
287
 
288
+ Array item validation is supported per-element with the `items` schema field:
289
+
290
+ ```ts
291
+ memorio.registerSchema('tags', {
292
+ type: 'array',
293
+ items: { type: 'string' },
294
+ })
295
+
296
+ state.tags = ['a', 'b'] // accepted
297
+ state.tags = [1, 2, 3] // rejected — items must be strings
298
+ ```
299
+
288
300
  ## Local-first sync
289
301
 
290
302
  The local application owns its data; the cloud is optional transport.
@@ -302,7 +314,9 @@ memorio.memory.configure({
302
314
  })
303
315
  ```
304
316
 
305
- Conflict resolution defaults to *higher confidence wins, then more recent `lastConfirmedAt`* — override it when you need a different rule:
317
+ When `replay()` pulls remote operations, entries are ordered causally by HLC timestamp. For
318
+ *concurrent* writes to the same key (an HLC tie) the **remote entry wins by default**; provide a
319
+ `resolveConflict` to override:
306
320
 
307
321
  ```ts
308
322
  memorio.memory.configure({
@@ -313,7 +327,9 @@ memorio.memory.configure({
313
327
  })
314
328
  ```
315
329
 
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.
330
+ When no resolver is supplied, Memorio logs a warn (dev only) and applies the remote entry on a
331
+ tie. The resolver only settles *client-side* divergence between what memorio has seen locally —
332
+ the provider is still responsible for the final server-side policy.
317
333
 
318
334
  ## Cross-platform support
319
335
 
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Memorio Basic Example
3
+ *
4
+ * This example shows the fundamental features of Memorio:
5
+ * - State management
6
+ * - Store (localStorage)
7
+ * - Session storage
8
+ * - Platform detection
9
+ *
10
+ * Run: npx ts-node examples/basic.ts
11
+ */
12
+
13
+ import 'memorio'
14
+
15
+ // ============================================
16
+ // PLATFORM DETECTION
17
+ // ============================================
18
+
19
+ console.debug('=== Platform Detection ===')
20
+ console.debug('Memorio version:', memorio.version)
21
+
22
+ // Check platform
23
+ if (memorio.isBrowser()) {
24
+ console.debug('Platform: Browser')
25
+ } else if (memorio.isNode()) {
26
+ console.debug('Platform: Node.js')
27
+ } else if (memorio.isDeno()) {
28
+ console.debug('Platform: Deno')
29
+ } else if (memorio.isEdge()) {
30
+ console.debug('Platform: Edge Worker')
31
+ }
32
+
33
+ // Get capabilities
34
+ const caps = memorio.getCapabilities()
35
+ console.debug('Capabilities:', {
36
+ platform: caps.platform,
37
+ hasLocalStorage: caps.hasLocalStorage,
38
+ hasSessionStorage: caps.hasSessionStorage,
39
+ hasIndexedDB: caps.hasIndexedDB
40
+ })
41
+
42
+ // ============================================
43
+ // STATE - In-memory reactive state
44
+ // ============================================
45
+
46
+ console.debug('\n=== State Management ===')
47
+
48
+ // Set some state values
49
+ state.name = 'Mario'
50
+ state.age = 30
51
+ state.isActive = true
52
+ state.profile = {
53
+ email: 'mario@example.com',
54
+ avatar: 'https://example.com/mario.png'
55
+ }
56
+
57
+ // Get state values
58
+ console.debug('Name:', state.name)
59
+ console.debug('Age:', state.age)
60
+ console.debug('Profile:', state.profile)
61
+
62
+ // List all states
63
+ console.debug('All states:', state.list)
64
+
65
+ // ============================================
66
+ // STORE - Persistent localStorage
67
+ // ============================================
68
+
69
+ console.debug('\n=== Store (Persistent Storage) ===')
70
+
71
+ // Check persistence
72
+ console.debug('Is persistent:', store.isPersistent) // true in browser, false in Node.js/Deno
73
+
74
+ // Save to localStorage
75
+ store.set('username', 'mario')
76
+ store.set('preferences', { theme: 'dark', language: 'en' })
77
+ store.set('lastLogin', Date.now())
78
+
79
+ // Read from localStorage
80
+ console.debug('Username:', store.get('username'))
81
+ console.debug('Preferences:', store.get('preferences'))
82
+
83
+ // Get storage size
84
+ console.debug('Storage size:', store.size(), 'bytes')
85
+
86
+ // ============================================
87
+ // SESSION - Temporary session storage
88
+ // ============================================
89
+
90
+ console.debug('\n=== Session (Temporary Storage) ===')
91
+
92
+ // Check persistence
93
+ console.debug('Is persistent:', session.isPersistent) // true in browser, false in Node.js/Deno
94
+
95
+ // Save session data (cleared when tab closes)
96
+ session.set('token', 'abc123xyz')
97
+ session.set('userId', 42)
98
+
99
+ // Read session data
100
+ console.debug('Token:', session.get('token'))
101
+ console.debug('User ID:', session.get('userId'))
102
+
103
+ // ============================================
104
+ // CLEANUP
105
+ // ============================================
106
+
107
+ // Clear specific items
108
+ store.remove('username')
109
+ session.remove('token')
110
+
111
+ // Clear all
112
+ store.removeAll()
113
+ session.removeAll()
114
+
115
+ console.debug('\nExample complete!')
@@ -0,0 +1,358 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Memorio Vanilla JS Example</title>
8
+ <style>
9
+ /* Simple CSS */
10
+ body {
11
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
12
+ max-width: 800px;
13
+ margin: 0 auto;
14
+ padding: 20px;
15
+ background: #f5f5f5;
16
+ }
17
+
18
+ .card {
19
+ background: white;
20
+ border-radius: 8px;
21
+ padding: 20px;
22
+ margin-bottom: 20px;
23
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
24
+ }
25
+
26
+ h1 {
27
+ color: #333;
28
+ }
29
+
30
+ h2 {
31
+ color: #666;
32
+ margin-top: 0;
33
+ }
34
+
35
+ button {
36
+ background: #007bff;
37
+ color: white;
38
+ border: none;
39
+ padding: 10px 20px;
40
+ border-radius: 4px;
41
+ cursor: pointer;
42
+ margin: 5px;
43
+ }
44
+
45
+ button:hover {
46
+ background: #0056b3;
47
+ }
48
+
49
+ button.danger {
50
+ background: #dc3545;
51
+ }
52
+
53
+ button.danger:hover {
54
+ background: #c82333;
55
+ }
56
+
57
+ button.success {
58
+ background: #28a745;
59
+ }
60
+
61
+ button.success:hover {
62
+ background: #218838;
63
+ }
64
+
65
+ input,
66
+ select {
67
+ padding: 8px;
68
+ border: 1px solid #ddd;
69
+ border-radius: 4px;
70
+ margin: 5px;
71
+ }
72
+
73
+ pre {
74
+ background: #f8f9fa;
75
+ padding: 10px;
76
+ border-radius: 4px;
77
+ overflow-x: auto;
78
+ }
79
+
80
+ .output {
81
+ background: #e9ecef;
82
+ padding: 10px;
83
+ border-radius: 4px;
84
+ font-family: monospace;
85
+ white-space: pre-wrap;
86
+ }
87
+
88
+ .grid {
89
+ display: grid;
90
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
91
+ gap: 10px;
92
+ }
93
+
94
+ .stat {
95
+ background: #e7f3ff;
96
+ padding: 10px;
97
+ border-radius: 4px;
98
+ text-align: center;
99
+ }
100
+
101
+ .stat-value {
102
+ font-size: 24px;
103
+ font-weight: bold;
104
+ color: #007bff;
105
+ }
106
+
107
+ .stat-label {
108
+ font-size: 12px;
109
+ color: #666;
110
+ }
111
+ </style>
112
+ </head>
113
+
114
+ <body>
115
+ <h1>🧠 Memorio Vanilla JS Demo</h1>
116
+
117
+ <!-- State Section -->
118
+ <div class="card">
119
+ <h2>1. State (Reactive)</h2>
120
+ <p>Global reactive state - changes trigger UI updates automatically</p>
121
+
122
+ <div class="grid">
123
+ <div class="stat">
124
+ <div class="stat-value" id="counter">0</div>
125
+ <div class="stat-label">Counter</div>
126
+ </div>
127
+ <div class="stat">
128
+ <div class="stat-value" id="username">-</div>
129
+ <div class="stat-label">Username</div>
130
+ </div>
131
+ <div class="stat">
132
+ <div class="stat-value" id="theme">light</div>
133
+ <div class="stat-label">Theme</div>
134
+ </div>
135
+ </div>
136
+
137
+ <br>
138
+ <button onclick="state.counter++">Increment Counter</button>
139
+ <button onclick="state.counter--">Decrement</button>
140
+ <button onclick="state.counter = 0">Reset</button>
141
+
142
+ <h3>Set Username:</h3>
143
+ <input type="text" id="nameInput" placeholder="Enter name">
144
+ <button onclick="state.username = document.getElementById('nameInput').value">Set</button>
145
+
146
+ <h3>Theme:</h3>
147
+ <button onclick="state.theme = 'light'">Light</button>
148
+ <button onclick="state.theme = 'dark'">Dark</button>
149
+
150
+ <h3>All State:</h3>
151
+ <pre id="stateOutput" class="output"></pre>
152
+ </div>
153
+
154
+ <!-- Store Section -->
155
+ <div class="card">
156
+ <h2>2. Store (localStorage)</h2>
157
+ <p>Permanent storage - survives page refresh</p>
158
+ <p>Persistent: <span id="storePersistent">checking...</span></p>
159
+
160
+ <input type="text" id="storeKey" placeholder="Key">
161
+ <input type="text" id="storeValue" placeholder="Value">
162
+ <button
163
+ onclick="store.set(document.getElementById('storeKey').value, document.getElementById('storeValue').value)">Save</button>
164
+ <button
165
+ onclick="document.getElementById('storeOutput').textContent = JSON.stringify(store.get(document.getElementById('storeKey').value), null, 2)">Get</button>
166
+ <button class="danger" onclick="store.remove(document.getElementById('storeKey').value)">Delete</button>
167
+
168
+ <h3>All Store Data:</h3>
169
+ <pre id="storeOutput" class="output"></pre>
170
+ </div>
171
+
172
+ <!-- Session Section -->
173
+ <div class="card">
174
+ <h2>3. Session (sessionStorage)</h2>
175
+ <p>Temporary storage - cleared when tab closes</p>
176
+ <p>Persistent: <span id="sessionPersistent">checking...</span></p>
177
+
178
+ <input type="text" id="sessionKey" placeholder="Key">
179
+ <input type="text" id="sessionValue" placeholder="Value">
180
+ <button
181
+ onclick="session.set(document.getElementById('sessionKey').value, document.getElementById('sessionValue').value)">Save</button>
182
+ <button
183
+ onclick="document.getElementById('sessionOutput').textContent = JSON.stringify(session.get(document.getElementById('sessionKey').value), null, 2)">Get</button>
184
+ <button class="danger" onclick="session.remove(document.getElementById('sessionKey').value)">Delete</button>
185
+
186
+ <h3>All Session Data:</h3>
187
+ <pre id="sessionOutput" class="output"></pre>
188
+ </div>
189
+
190
+ <!-- Cache Section -->
191
+ <div class="card">
192
+ <h2>4. Cache (In-Memory)</h2>
193
+ <p>Temporary cache - cleared on refresh</p>
194
+
195
+ <input type="text" id="cacheKey" placeholder="Key">
196
+ <input type="text" id="cacheValue" placeholder="Value">
197
+ <button
198
+ onclick="cache.set(document.getElementById('cacheKey').value, document.getElementById('cacheValue').value)">Save</button>
199
+ <button
200
+ onclick="document.getElementById('cacheOutput').textContent = JSON.stringify(cache.get(document.getElementById('cacheKey').value), null, 2)">Get</button>
201
+ <button class="danger" onclick="cache.remove(document.getElementById('cacheKey').value)">Delete</button>
202
+
203
+ <h3>All Cache Data:</h3>
204
+ <pre id="cacheOutput" class="output"></pre>
205
+ </div>
206
+
207
+ <!-- Observer Section -->
208
+ <div class="card">
209
+ <h2>5. Observer (Auto-Reactive)</h2>
210
+ <p>Watch for state changes automatically</p>
211
+
212
+ <button class="success" id="startObserver">Start Observer</button>
213
+ <button class="danger" id="stopObserver">Stop Observer</button>
214
+ <button onclick="state.testValue = Math.random()">Trigger Change</button>
215
+
216
+ <h3>Observer Log:</h3>
217
+ <pre id="observerLog" class="output"></pre>
218
+ </div>
219
+
220
+ <!-- Platform Info -->
221
+ <div class="card">
222
+ <h2>6. Platform Info</h2>
223
+ <div id="platformInfo" class="output"></div>
224
+ </div>
225
+
226
+ <!-- Load Memorio -->
227
+ <script src="https://cdn.jsdelivr.net/npm/memorio/index.cjs"></script>
228
+
229
+ <script>
230
+ // ============================================
231
+ // MEMORIO VANILLA JS EXAMPLE
232
+ // ============================================
233
+
234
+ // 1. REACTIVE STATE
235
+ // -----------------
236
+
237
+ // Initialize state
238
+ state.counter = 0
239
+ state.username = 'Guest'
240
+ state.theme = 'light'
241
+ state.testValue = 0
242
+
243
+ // Update UI when state changes
244
+ function updateStateUI() {
245
+ document.getElementById('counter').textContent = state.counter
246
+ document.getElementById('username').textContent = state.username
247
+ document.getElementById('theme').textContent = state.theme
248
+ document.getElementById('stateOutput').textContent = JSON.stringify({
249
+ counter: state.counter,
250
+ username: state.username,
251
+ theme: state.theme,
252
+ testValue: state.testValue
253
+ }, null, 2)
254
+ }
255
+
256
+ // Use observer to react to changes
257
+ useObserver(() => {
258
+ updateStateUI()
259
+ document.body.className = state.theme
260
+ }, [state.counter, state.username, state.theme, state.testValue])
261
+
262
+ // 2. PERSISTENT STORE
263
+ // -------------------
264
+
265
+ // Check persistence
266
+ document.getElementById('storePersistent').textContent = store.isPersistent ? '✅ Yes (localStorage)' : '❌ No (memory)'
267
+
268
+ // Save sample data
269
+ store.set('visits', '0')
270
+ store.set('lastVisit', new Date().toISOString())
271
+
272
+ function updateStoreUI() {
273
+ document.getElementById('storeOutput').textContent = 'Visits: ' + store.get('visits') + '\nLast Visit: ' + store.get('lastVisit')
274
+ }
275
+ updateStoreUI()
276
+
277
+ // Increment visits
278
+ let visits = parseInt(store.get('visits') || '0')
279
+ store.set('visits', (visits + 1).toString())
280
+
281
+ // 3. SESSION STORAGE
282
+ // ------------------
283
+
284
+ document.getElementById('sessionPersistent').textContent = session.isPersistent ? '✅ Yes (sessionStorage)' : '❌ No (memory)'
285
+
286
+ session.set('pageViews', '0')
287
+
288
+ function updateSessionUI() {
289
+ document.getElementById('sessionOutput').textContent = 'Page Views: ' + session.get('pageViews')
290
+ }
291
+ updateSessionUI()
292
+
293
+ // Increment page views
294
+ let pageViews = parseInt(session.get('pageViews') || '0')
295
+ session.set('pageViews', (pageViews + 1).toString())
296
+
297
+ // 4. CACHE (In-Memory)
298
+ // --------------------
299
+
300
+ cache.set('tempData', { computed: true, timestamp: Date.now() })
301
+
302
+ function updateCacheUI() {
303
+ document.getElementById('cacheOutput').textContent = JSON.stringify(cache.get('tempData'), null, 2)
304
+ }
305
+ updateCacheUI()
306
+
307
+ // 5. OBSERVER
308
+ // -----------
309
+
310
+ let observerActive = false
311
+ const logEl = document.getElementById('observerLog')
312
+
313
+ document.getElementById('startObserver').onclick = () => {
314
+ observerActive = true
315
+ logEl.textContent += 'Observer started!\n'
316
+ }
317
+
318
+ document.getElementById('stopObserver').onclick = () => {
319
+ observerActive = false
320
+ logEl.textContent += 'Observer stopped!\n'
321
+ }
322
+
323
+ // This creates a persistent observer
324
+ useObserver((newVal, oldVal) => {
325
+ if (observerActive) {
326
+ logEl.textContent += `Changed: ${oldVal} → ${newVal}\n`
327
+ logEl.scrollTop = logEl.scrollHeight
328
+ }
329
+ }, [state.testValue])
330
+
331
+ // 6. PLATFORM INFO
332
+ // ----------------
333
+
334
+ const platformInfo = document.getElementById('platformInfo')
335
+ platformInfo.textContent = `
336
+ Platform Detection:
337
+ -----------------
338
+ Browser: ${memorio.isBrowser() ? '✅' : '❌'}
339
+ Node.js: ${memorio.isNode() ? '✅' : '❌'}
340
+ Deno: ${memorio.isDeno() ? '✅' : '❌'}
341
+ Edge: ${memorio.isEdge() ? '✅' : '❌'}
342
+
343
+ Capabilities:
344
+ -------------
345
+ localStorage: ${memorio.getCapabilities().hasLocalStorage ? '✅' : '❌'}
346
+ sessionStorage: ${memorio.getCapabilities().hasSessionStorage ? '✅' : '❌'}
347
+ IndexedDB: ${memorio.getCapabilities().hasIndexedDB ? '✅' : '❌'}
348
+
349
+ Session ID: ${memorio.getCapabilities().sessionId.substring(0, 8)}...
350
+ `.trim()
351
+
352
+ // Log welcome
353
+ console.debug('🧠 Memorio loaded!')
354
+ console.debug('Platform:', memorio.getCapabilities().platform)
355
+ </script>
356
+ </body>
357
+
358
+ </html>