memorio 4.9.10 → 4.9.31

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.
@@ -1,57 +1,57 @@
1
- /**
2
- * cross-platform-guards.ts
3
- *
4
- * Scenario: code that might run in a browser, Node.js, Deno, or an edge
5
- * runtime, and needs to behave correctly (not just "not crash") in each.
6
- *
7
- * Prefer `getCapabilities()` over branching on `isBrowser()`/`isNode()`
8
- * alone — capability support can differ *within* a platform category
9
- * (some edge runtimes expose `localStorage`, some don't).
10
- */
11
- import { memorio, idb, store, session } from 'memorio'
12
-
13
- export async function savePreferences(prefs: Record<string, unknown>) {
14
- const caps = memorio.getCapabilities()
15
-
16
- // store/session silently fall back to a non-durable in-memory Map outside
17
- // the browser — that's not a crash, but it's also not what "save" implies
18
- // to a caller. Surface the distinction instead of hiding it.
19
- store.set('preferences', prefs)
20
-
21
- if (!store.isPersistent) {
22
- console.warn(
23
- '[preferences] store is not durable in this environment (%s) — ' +
24
- 'data will not survive a process restart',
25
- caps.platform
26
- )
27
- }
28
-
29
- return store.isPersistent
30
- }
31
-
32
- export async function saveStructuredRecord(dbName: string, table: string, record: unknown) {
33
- // idb is disabled outright in Node.js/Deno (no-op + warning). Guard
34
- // explicitly instead of relying on that warning reaching anyone.
35
- if (!idb.db.support()) {
36
- throw new Error(
37
- `[saveStructuredRecord] IndexedDB is not available in this environment ` +
38
- `(platform: ${memorio.getCapabilities().platform}). ` +
39
- `Use sqlite or store instead when running outside a browser.`
40
- )
41
- }
42
-
43
- await idb.db.create(dbName)
44
- await idb.table.create(dbName, table)
45
- await idb.data.set(dbName, table, record)
46
- }
47
-
48
- export function describeEnvironment() {
49
- const caps = memorio.getCapabilities()
50
-
51
- return {
52
- platform: caps.platform,
53
- canPersistAcrossTabs: caps.hasLocalStorage,
54
- canPersistThisTabOnly: session.isPersistent,
55
- canUseStructuredStorage: caps.hasIndexedDB,
56
- }
57
- }
1
+ /**
2
+ * cross-platform-guards.ts
3
+ *
4
+ * Scenario: code that might run in a browser, Node.js, Deno, or an edge
5
+ * runtime, and needs to behave correctly (not just "not crash") in each.
6
+ *
7
+ * Prefer `getCapabilities()` over branching on `isBrowser()`/`isNode()`
8
+ * alone - capability support can differ *within* a platform category
9
+ * (some edge runtimes expose `localStorage`, some don't).
10
+ */
11
+ import { memorio, idb, store, session } from 'memorio'
12
+
13
+ export async function savePreferences(prefs: Record<string, unknown>) {
14
+ const caps = memorio.getCapabilities()
15
+
16
+ // store/session silently fall back to a non-durable in-memory Map outside
17
+ // the browser - that's not a crash, but it's also not what "save" implies
18
+ // to a caller. Surface the distinction instead of hiding it.
19
+ store.set('preferences', prefs)
20
+
21
+ if (!store.isPersistent) {
22
+ console.warn(
23
+ '[preferences] store is not durable in this environment (%s) - ' +
24
+ 'data will not survive a process restart',
25
+ caps.platform
26
+ )
27
+ }
28
+
29
+ return store.isPersistent
30
+ }
31
+
32
+ export async function saveStructuredRecord(dbName: string, table: string, record: unknown) {
33
+ // idb is disabled outright in Node.js/Deno (no-op + warning). Guard
34
+ // explicitly instead of relying on that warning reaching anyone.
35
+ if (!idb.db.support()) {
36
+ throw new Error(
37
+ `[saveStructuredRecord] IndexedDB is not available in this environment ` +
38
+ `(platform: ${memorio.getCapabilities().platform}). ` +
39
+ `Use sqlite or store instead when running outside a browser.`
40
+ )
41
+ }
42
+
43
+ await idb.db.create(dbName)
44
+ await idb.table.create(dbName, table)
45
+ await idb.data.set(dbName, table, record)
46
+ }
47
+
48
+ export function describeEnvironment() {
49
+ const caps = memorio.getCapabilities()
50
+
51
+ return {
52
+ platform: caps.platform,
53
+ canPersistAcrossTabs: caps.hasLocalStorage,
54
+ canPersistThisTabOnly: session.isPersistent,
55
+ canUseStructuredStorage: caps.hasIndexedDB,
56
+ }
57
+ }
@@ -1,44 +1,44 @@
1
- /**
2
- * multi-tenant-context.ts
3
- *
4
- * Scenario: a server-side handler (API route, edge function) that may
5
- * process requests from different users/tenants in the same process.
6
- *
7
- * `state` is a shared global namespace by default — writing to it directly
8
- * in a multi-request handler leaks data between requests. Use an explicit
9
- * context per request instead.
10
- */
11
- import { memorio } from 'memorio'
12
-
13
- interface RequestContext {
14
- userId: string
15
- tenantId: string
16
- }
17
-
18
- export function handleRequest(req: RequestContext, payload: { language: string }) {
19
- // Derive the context id from trusted, server-verified data — never from a
20
- // raw client-supplied header or query param, since contexts are a
21
- // key-prefix convention, not a hard isolation boundary.
22
- const contextId = `tenant:${req.tenantId}:user:${req.userId}`
23
- const ctx = memorio.createContext(contextId)
24
-
25
- // All reads/writes go through ctx.state, not the bare global `state` —
26
- // this is what actually keeps this request's data from leaking into the
27
- // next one handled by the same process.
28
- ctx.state.language = payload.language
29
-
30
- return { language: ctx.state.language }
31
- }
32
-
33
- // Cleanup for long-lived processes that create many short-lived contexts
34
- // (e.g. one per request in a busy server): drop the context when the
35
- // request is done, or it accumulates for the life of the process.
36
- export function teardownRequest(req: RequestContext) {
37
- const contextId = `tenant:${req.tenantId}:user:${req.userId}`
38
- memorio.deleteContext(contextId)
39
- }
40
-
41
- // NOTE: context isolation here is organizational (key-prefixing under the
42
- // same storage), not a security boundary. Don't rely on it as the only
43
- // thing preventing one tenant from reaching another tenant's data — enforce
44
- // that at the auth/backend layer as well.
1
+ /**
2
+ * multi-tenant-context.ts
3
+ *
4
+ * Scenario: a server-side handler (API route, edge function) that may
5
+ * process requests from different users/tenants in the same process.
6
+ *
7
+ * `state` is a shared global namespace by default - writing to it directly
8
+ * in a multi-request handler leaks data between requests. Use an explicit
9
+ * context per request instead.
10
+ */
11
+ import { memorio } from 'memorio'
12
+
13
+ interface RequestContext {
14
+ userId: string
15
+ tenantId: string
16
+ }
17
+
18
+ export function handleRequest(req: RequestContext, payload: { language: string }) {
19
+ // Derive the context id from trusted, server-verified data - never from a
20
+ // raw client-supplied header or query param, since contexts are a
21
+ // key-prefix convention, not a hard isolation boundary.
22
+ const contextId = `tenant:${req.tenantId}:user:${req.userId}`
23
+ const ctx = memorio.createContext(contextId)
24
+
25
+ // All reads/writes go through ctx.state, not the bare global `state` -
26
+ // this is what actually keeps this request's data from leaking into the
27
+ // next one handled by the same process.
28
+ ctx.state.language = payload.language
29
+
30
+ return { language: ctx.state.language }
31
+ }
32
+
33
+ // Cleanup for long-lived processes that create many short-lived contexts
34
+ // (e.g. one per request in a busy server): drop the context when the
35
+ // request is done, or it accumulates for the life of the process.
36
+ export function teardownRequest(req: RequestContext) {
37
+ const contextId = `tenant:${req.tenantId}:user:${req.userId}`
38
+ memorio.deleteContext(contextId)
39
+ }
40
+
41
+ // NOTE: context isolation here is organizational (key-prefixing under the
42
+ // same storage), not a security boundary. Don't rely on it as the only
43
+ // thing preventing one tenant from reaching another tenant's data - enforce
44
+ // that at the auth/backend layer as well.
@@ -1,63 +1,63 @@
1
- /**
2
- * react-observer.tsx
3
- *
4
- * Scenario: a React component reading reactive `state`. Shows both
5
- * `useObserver` modes and when to prefer each, plus `typed<T>()` +
6
- * `registerSchema()` for the paths you don't want to fail silently on a
7
- * typo or a later rename.
8
- */
9
- import 'memorio'
10
- import { useReducer } from 'react'
11
-
12
- interface AppState {
13
- counter: number
14
- user: { name: string; email: string }
15
- }
16
-
17
- // Compile-time safety: same Proxy as the global `state`, no duplicated store.
18
- const app = memorio.typed<AppState>()
19
-
20
- // Runtime safety: reject writes that don't match the shape, independent of
21
- // whatever calls it (including code you didn't write).
22
- memorio.registerSchema('user', {
23
- type: 'object',
24
- required: ['name', 'email'],
25
- properties: {
26
- name: { type: 'string', min: 1 },
27
- email: { type: 'string', pattern: /^[^@]+@[^@]+$/ },
28
- },
29
- })
30
-
31
- export function Counter() {
32
- const [, forceUpdate] = useReducer((x: number) => x + 1, 0)
33
-
34
- // Auto-discovery mode: re-runs when any state path *touched inside the
35
- // callback* changes. Convenient, but re-runs on anything you read there —
36
- // don't reach for it in a callback that reads many unrelated paths.
37
- useObserver(() => {
38
- forceUpdate()
39
- }, state.counter)
40
-
41
- return (
42
- <div>
43
- <span>Count: {app.counter}</span>
44
- <button onClick={() => { app.counter += 1 }}>+1</button>
45
- </div>
46
- )
47
- }
48
-
49
- export function UserBadge() {
50
- const [, forceUpdate] = useReducer((x: number) => x + 1, 0)
51
-
52
- // Explicit deps mode: only re-runs when the listed path(s) change — use
53
- // this when you want precise control instead of auto-discovery.
54
- useObserver(() => {
55
- forceUpdate()
56
- }, [state.user])
57
-
58
- return <span>{app.user?.name ?? 'Guest'}</span>
59
- }
60
-
61
- // Rejected at write time by the schema registered above:
62
- // app.user = { name: 'Sara' } // ❌ missing "email"
63
- // app.user = { name: 'Sara', email: 'x@y.com' } // ✅
1
+ /**
2
+ * react-observer.tsx
3
+ *
4
+ * Scenario: a React component reading reactive `state`. Shows both
5
+ * `useObserver` modes and when to prefer each, plus `typed<T>()` +
6
+ * `registerSchema()` for the paths you don't want to fail silently on a
7
+ * typo or a later rename.
8
+ */
9
+ import 'memorio'
10
+ import { useReducer } from 'react'
11
+
12
+ interface AppState {
13
+ counter: number
14
+ user: { name: string; email: string }
15
+ }
16
+
17
+ // Compile-time safety: same Proxy as the global `state`, no duplicated store.
18
+ const app = memorio.typed<AppState>()
19
+
20
+ // Runtime safety: reject writes that don't match the shape, independent of
21
+ // whatever calls it (including code you didn't write).
22
+ memorio.registerSchema('user', {
23
+ type: 'object',
24
+ required: ['name', 'email'],
25
+ properties: {
26
+ name: { type: 'string', min: 1 },
27
+ email: { type: 'string', pattern: /^[^@]+@[^@]+$/ },
28
+ },
29
+ })
30
+
31
+ export function Counter() {
32
+ const [, forceUpdate] = useReducer((x: number) => x + 1, 0)
33
+
34
+ // Auto-discovery mode: re-runs when any state path *touched inside the
35
+ // callback* changes. Convenient, but re-runs on anything you read there -
36
+ // don't reach for it in a callback that reads many unrelated paths.
37
+ useObserver(() => {
38
+ forceUpdate()
39
+ }, state.counter)
40
+
41
+ return (
42
+ <div>
43
+ <span>Count: {app.counter}</span>
44
+ <button onClick={() => { app.counter += 1 }}>+1</button>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export function UserBadge() {
50
+ const [, forceUpdate] = useReducer((x: number) => x + 1, 0)
51
+
52
+ // Explicit deps mode: only re-runs when the listed path(s) change - use
53
+ // this when you want precise control instead of auto-discovery.
54
+ useObserver(() => {
55
+ forceUpdate()
56
+ }, [state.user])
57
+
58
+ return <span>{app.user?.name ?? 'Guest'}</span>
59
+ }
60
+
61
+ // Rejected at write time by the schema registered above:
62
+ // app.user = { name: 'Sara' } // ❌ missing "email"
63
+ // app.user = { name: 'Sara', email: 'x@y.com' } // ✅
@@ -1,60 +1,60 @@
1
- /**
2
- * semantic-memory.ts
3
- *
4
- * Scenario: an LLM-backed app that needs to remember facts about a user
5
- * with confidence, source, and expiry — and later retrieve only what's
6
- * relevant to the current task.
7
- *
8
- * Important: `memory.context()` ranks by tags/type/confidence/recency.
9
- * It is NOT embedding-based semantic search — don't build a feature that
10
- * assumes free-text meaning-based retrieval on top of this alone.
11
- */
12
- import { memorio } from 'memorio'
13
-
14
- export async function rememberPreference(
15
- key: string,
16
- value: unknown,
17
- opts: { confidence: number; source: string; tags: string[] }
18
- ) {
19
- await memorio.memory.remember(key, value, {
20
- type: 'preference',
21
- scope: 'local',
22
- ...opts,
23
- })
24
- }
25
-
26
- export async function correctPreference(key: string, newValue: unknown, confidence: number) {
27
- // update() supersedes, it doesn't overwrite — the old entry becomes
28
- // status: 'superseded' and stays queryable for history/audit purposes.
29
- await memorio.memory.update(key, newValue, { confidence })
30
- }
31
-
32
- export async function getRelevantContext(tags: string[]) {
33
- // This is rule-based retrieval, not semantic similarity search. It's the
34
- // right tool for "give me what's tagged/confident/recent enough," not for
35
- // "find the memory that means the same thing as this new sentence."
36
- return memorio.memory.context({
37
- tags,
38
- types: ['preference', 'decision'],
39
- minConfidence: 0.7,
40
- maxEntries: 10,
41
- })
42
- }
43
-
44
- // If the task genuinely needs meaning-based retrieval over free text (e.g.
45
- // "find prior notes about a similar problem" rather than "find notes tagged
46
- // X with confidence > Y"), pair this with a dedicated embedding store for
47
- // the similarity search, and keep using memorio.memory for the lifecycle
48
- // metadata (confidence, TTL, supersession) on top of whatever it finds —
49
- // don't simulate similarity search with context() alone.
50
-
51
- // Usage:
52
- // await rememberPreference('user.language', 'Italian', {
53
- // confidence: 0.92,
54
- // source: 'conversation',
55
- // tags: ['user', 'ui'],
56
- // })
57
- //
58
- // await correctPreference('user.language', 'English', 0.95)
59
- //
60
- // const ctx = await getRelevantContext(['user'])
1
+ /**
2
+ * semantic-memory.ts
3
+ *
4
+ * Scenario: an LLM-backed app that needs to remember facts about a user
5
+ * with confidence, source, and expiry - and later retrieve only what's
6
+ * relevant to the current task.
7
+ *
8
+ * Important: `memory.context()` ranks by tags/type/confidence/recency.
9
+ * It is NOT embedding-based semantic search - don't build a feature that
10
+ * assumes free-text meaning-based retrieval on top of this alone.
11
+ */
12
+ import { memorio } from 'memorio'
13
+
14
+ export async function rememberPreference(
15
+ key: string,
16
+ value: unknown,
17
+ opts: { confidence: number; source: string; tags: string[] }
18
+ ) {
19
+ await memorio.memory.remember(key, value, {
20
+ type: 'preference',
21
+ scope: 'local',
22
+ ...opts,
23
+ })
24
+ }
25
+
26
+ export async function correctPreference(key: string, newValue: unknown, confidence: number) {
27
+ // update() supersedes, it doesn't overwrite - the old entry becomes
28
+ // status: 'superseded' and stays queryable for history/audit purposes.
29
+ await memorio.memory.update(key, newValue, { confidence })
30
+ }
31
+
32
+ export async function getRelevantContext(tags: string[]) {
33
+ // This is rule-based retrieval, not semantic similarity search. It's the
34
+ // right tool for "give me what's tagged/confident/recent enough," not for
35
+ // "find the memory that means the same thing as this new sentence."
36
+ return memorio.memory.context({
37
+ tags,
38
+ types: ['preference', 'decision'],
39
+ minConfidence: 0.7,
40
+ maxEntries: 10,
41
+ })
42
+ }
43
+
44
+ // If the task genuinely needs meaning-based retrieval over free text (e.g.
45
+ // "find prior notes about a similar problem" rather than "find notes tagged
46
+ // X with confidence > Y"), pair this with a dedicated embedding store for
47
+ // the similarity search, and keep using memorio.memory for the lifecycle
48
+ // metadata (confidence, TTL, supersession) on top of whatever it finds -
49
+ // don't simulate similarity search with context() alone.
50
+
51
+ // Usage:
52
+ // await rememberPreference('user.language', 'Italian', {
53
+ // confidence: 0.92,
54
+ // source: 'conversation',
55
+ // tags: ['user', 'ui'],
56
+ // })
57
+ //
58
+ // await correctPreference('user.language', 'English', 0.95)
59
+ //
60
+ // const ctx = await getRelevantContext(['user'])
@@ -73,7 +73,7 @@ console.debug('Cart total:', total)
73
73
  // ============================================
74
74
 
75
75
  // Get session storage size
76
- console.debug('Session size:', session.size(), 'bytes')
76
+ console.debug('Session size:', session.size(), 'kilobytes')
77
77
 
78
78
  // ============================================
79
79
  // CLEANUP
@@ -1,57 +1,57 @@
1
- /**
2
- * sqlite-batched-writes.ts
3
- *
4
- * Scenario: importing or writing many rows into memorio's sqlite layer.
5
- *
6
- * Persistence there serializes the ENTIRE database on every flush — not
7
- * incremental. Calling a persisting write inside a per-row loop is the
8
- * single most common way to accidentally make this layer slow.
9
- */
10
- import { memorio } from 'memorio'
11
-
12
- interface UserRow {
13
- id: number
14
- name: string
15
- role: string
16
- }
17
-
18
- export async function importUsers(users: UserRow[]) {
19
- await memorio.sqlite.ready
20
- await memorio.sqlite.db.create('app', { persistence: true })
21
-
22
- await memorio.sqlite.query.run(
23
- 'app',
24
- `CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, role TEXT)`
25
- )
26
-
27
- // Bad: this would serialize the whole DB after every single insert.
28
- //
29
- // for (const user of users) {
30
- // await memorio.sqlite.query.run('app', 'INSERT INTO users ...', [...])
31
- // await memorio.sqlite.db.flush('app') // <- don't do this per row
32
- // }
33
-
34
- // Good: run every insert first, persist once after the batch.
35
- for (const user of users) {
36
- await memorio.sqlite.query.run(
37
- 'app',
38
- `INSERT INTO users (id, name, role) VALUES (?, ?, ?)`,
39
- [user.id, user.name, user.role]
40
- )
41
- }
42
-
43
- // Persist explicitly, once, after the whole batch — check the installed
44
- // version's API for the exact flush/persist call name if it differs from
45
- // automatic persistence-on-close.
46
- const admins = await memorio.sqlite.query.select(
47
- 'app',
48
- `SELECT * FROM users WHERE role = ?`,
49
- ['admin']
50
- )
51
-
52
- return admins
53
- }
54
-
55
- // For read-heavy or scratch-space use where durability doesn't matter,
56
- // skip { persistence: true } entirely — sqlite runs in memory by default,
57
- // which avoids the serialize cost altogether.
1
+ /**
2
+ * sqlite-batched-writes.ts
3
+ *
4
+ * Scenario: importing or writing many rows into memorio's sqlite layer.
5
+ *
6
+ * Persistence there serializes the ENTIRE database on every flush - not
7
+ * incremental. Calling a persisting write inside a per-row loop is the
8
+ * single most common way to accidentally make this layer slow.
9
+ */
10
+ import { memorio } from 'memorio'
11
+
12
+ interface UserRow {
13
+ id: number
14
+ name: string
15
+ role: string
16
+ }
17
+
18
+ export async function importUsers(users: UserRow[]) {
19
+ await memorio.sqlite.ready
20
+ await memorio.sqlite.db.create('app', { persistence: true })
21
+
22
+ await memorio.sqlite.query.run(
23
+ 'app',
24
+ `CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, role TEXT)`
25
+ )
26
+
27
+ // Bad: this would serialize the whole DB after every single insert.
28
+ //
29
+ // for (const user of users) {
30
+ // await memorio.sqlite.query.run('app', 'INSERT INTO users ...', [...])
31
+ // await memorio.sqlite.db.flush('app') // <- don't do this per row
32
+ // }
33
+
34
+ // Good: run every insert first, persist once after the batch.
35
+ for (const user of users) {
36
+ await memorio.sqlite.query.run(
37
+ 'app',
38
+ `INSERT INTO users (id, name, role) VALUES (?, ?, ?)`,
39
+ [user.id, user.name, user.role]
40
+ )
41
+ }
42
+
43
+ // Persist explicitly, once, after the whole batch - check the installed
44
+ // version's API for the exact flush/persist call name if it differs from
45
+ // automatic persistence-on-close.
46
+ const admins = await memorio.sqlite.query.select(
47
+ 'app',
48
+ `SELECT * FROM users WHERE role = ?`,
49
+ ['admin']
50
+ )
51
+
52
+ return admins
53
+ }
54
+
55
+ // For read-heavy or scratch-space use where durability doesn't matter,
56
+ // skip { persistence: true } entirely - sqlite runs in memory by default,
57
+ // which avoids the serialize cost altogether.
@@ -51,7 +51,7 @@ if (savedPrefs) {
51
51
 
52
52
  // Get storage size
53
53
  const currentSize = store.size()
54
- console.debug('Current storage size:', currentSize, 'bytes')
54
+ console.debug('Current storage size:', currentSize, 'kilobytes')
55
55
 
56
56
  // ============================================
57
57
  // ALIAS METHODS