memorio 4.9.35 → 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.
Files changed (76) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +307 -359
  3. package/SECURITY.md +17 -1
  4. package/SUMMARY.md +59 -45
  5. package/adr/001-state-proxy-model.md +96 -0
  6. package/adr/002-observer-semantics.md +180 -0
  7. package/adr/003-deep-mutation-semantics.md +129 -0
  8. package/adr/004-array-mutation-semantics.md +128 -0
  9. package/adr/005-scheduler-contract.md +149 -0
  10. package/adr/006-context-isolation.md +92 -0
  11. package/adr/007-mutation-records.md +118 -0
  12. package/adr/008-transactions.md +106 -0
  13. package/adr/009-history-model.md +110 -0
  14. package/adr/README.md +46 -0
  15. package/adr/template.md +49 -0
  16. package/examples/basic.ts +115 -115
  17. package/examples/browser-vanilla.html +358 -358
  18. package/examples/cache.ts +72 -72
  19. package/examples/cross-platform-guards.ts +57 -57
  20. package/examples/history.ts +104 -0
  21. package/examples/idb.ts +109 -109
  22. package/examples/multi-tenant-context.ts +44 -44
  23. package/examples/node-server.ts +308 -308
  24. package/examples/observer.ts +60 -60
  25. package/examples/platform.ts +115 -115
  26. package/examples/react-app.tsx +362 -362
  27. package/examples/react-observer.tsx +63 -63
  28. package/examples/semantic-memory.ts +60 -60
  29. package/examples/session-advanced.ts +91 -91
  30. package/examples/sqlite-batched-writes.ts +57 -57
  31. package/examples/state-advanced.ts +89 -89
  32. package/examples/store-advanced.ts +117 -117
  33. package/examples/sync.ts +90 -0
  34. package/examples/typed-and-schema.ts +102 -100
  35. package/examples/useObserver.tsx +140 -141
  36. package/global.cjs +4594 -0
  37. package/global.d.ts +8 -0
  38. package/global.js +4532 -0
  39. package/index.cjs +700 -678
  40. package/index.d.ts +1 -0
  41. package/index.js +680 -677
  42. package/llms.txt +72 -4
  43. package/markdown/AUDIT-REPORT.md +135 -0
  44. package/markdown/CACHE.md +100 -0
  45. package/markdown/CHANGELOG.md +243 -0
  46. package/markdown/DEVTOOLS.md +129 -0
  47. package/markdown/DISPATCH.md +177 -0
  48. package/markdown/HISTORY.md +199 -0
  49. package/markdown/IDB.md +178 -0
  50. package/markdown/IMPORT.md +153 -0
  51. package/markdown/INSPECT.md +123 -0
  52. package/markdown/LOGGER.md +154 -0
  53. package/markdown/MEMORY-ATTACHMENT.md +96 -0
  54. package/markdown/MEMORY.md +162 -0
  55. package/markdown/OBSERVER.md +209 -0
  56. package/markdown/PLATFORM.md +271 -0
  57. package/markdown/PROJECT.md +311 -0
  58. package/markdown/SCHEMA.md +176 -0
  59. package/markdown/SECURITY.md +330 -0
  60. package/markdown/SESSION.md +165 -0
  61. package/markdown/SQLITE.md +190 -0
  62. package/markdown/STATE.md +160 -0
  63. package/markdown/STORE.md +171 -0
  64. package/markdown/SYNC.md +319 -0
  65. package/markdown/TYPED.md +165 -0
  66. package/markdown/USEOBSERVER.md +257 -0
  67. package/modules/redux.cjs +320 -167
  68. package/modules/redux.cjs.map +1 -1
  69. package/modules/redux.js +320 -167
  70. package/modules/redux.js.map +1 -1
  71. package/package.json +13 -3
  72. package/types/env.d.ts +19 -9
  73. package/types/exports.d.ts +20 -0
  74. package/types/history.d.ts +13 -1
  75. package/types/memorio.d.ts +17 -5
  76. package/types/mutation.d.ts +75 -0
package/examples/cache.ts CHANGED
@@ -1,72 +1,72 @@
1
- /**
2
- * Memorio Cache Example
3
- *
4
- * This example demonstrates the cache module for in-memory storage.
5
- * Cache is perfect for temporary data that doesn't need persistence.
6
- *
7
- * Run: npx ts-node examples/cache.ts
8
- */
9
-
10
- import 'memorio'
11
-
12
- // ============================================
13
- // BASIC CACHE OPERATIONS
14
- // ============================================
15
-
16
- // Set values
17
- cache.set('username', 'Mario')
18
- cache.set('score', 1500)
19
- cache.set('player', { name: 'Mario', level: 5 })
20
-
21
- // Get values
22
- console.debug('Username:', cache.get('username'))
23
- console.debug('Score:', cache.get('score'))
24
- console.debug('Player:', cache.get('player'))
25
-
26
- // Direct property access
27
- cache.tempData = 'Hello'
28
- console.debug('Temp data:', cache.tempData)
29
-
30
- // ============================================
31
- // CACHE WITH OBJECTS
32
- // ============================================
33
-
34
- // Store complex objects
35
- cache.set('gameState', {
36
- level: 3,
37
- score: 2500,
38
- inventory: ['sword', 'shield', 'potion'],
39
- position: { x: 100, y: 200 }
40
- })
41
-
42
- const gameState = cache.get('gameState')
43
- console.debug('Game level:', gameState?.level)
44
- console.debug('Inventory:', gameState?.inventory)
45
-
46
- // ============================================
47
- // CACHE SIZE
48
- // ============================================
49
-
50
- // Add multiple items
51
- for (let i = 0; i < 10; i++) {
52
- cache.set(`item_${i}`, { id: i, data: `item-${i}` })
53
- }
54
-
55
- console.debug('Cache keys:', Object.keys(cache))
56
-
57
- // ============================================
58
- // CLEANUP
59
- // ============================================
60
-
61
- // Remove single item
62
- cache.remove('username')
63
-
64
- // Clear all cache
65
- cache.removeAll()
66
-
67
- // Or use clearAll alias
68
- // cache.clearAll()
69
-
70
- console.debug('Cache after clear:', cache.get('score'))
71
-
72
- console.debug('Cache example complete!')
1
+ /**
2
+ * Memorio Cache Example
3
+ *
4
+ * This example demonstrates the cache module for in-memory storage.
5
+ * Cache is perfect for temporary data that doesn't need persistence.
6
+ *
7
+ * Run: npx ts-node examples/cache.ts
8
+ */
9
+
10
+ import { cache } from 'memorio'
11
+
12
+ // ============================================
13
+ // BASIC CACHE OPERATIONS
14
+ // ============================================
15
+
16
+ // Set values
17
+ cache.set('username', 'Mario')
18
+ cache.set('score', 1500)
19
+ cache.set('player', { name: 'Mario', level: 5 })
20
+
21
+ // Get values
22
+ console.debug('Username:', cache.get('username'))
23
+ console.debug('Score:', cache.get('score'))
24
+ console.debug('Player:', cache.get('player'))
25
+
26
+ // Direct property access
27
+ cache.tempData = 'Hello'
28
+ console.debug('Temp data:', cache.tempData)
29
+
30
+ // ============================================
31
+ // CACHE WITH OBJECTS
32
+ // ============================================
33
+
34
+ // Store complex objects
35
+ cache.set('gameState', {
36
+ level: 3,
37
+ score: 2500,
38
+ inventory: ['sword', 'shield', 'potion'],
39
+ position: { x: 100, y: 200 }
40
+ })
41
+
42
+ const gameState = cache.get('gameState')
43
+ console.debug('Game level:', gameState?.level)
44
+ console.debug('Inventory:', gameState?.inventory)
45
+
46
+ // ============================================
47
+ // CACHE SIZE
48
+ // ============================================
49
+
50
+ // Add multiple items
51
+ for (let i = 0; i < 10; i++) {
52
+ cache.set(`item_${i}`, { id: i, data: `item-${i}` })
53
+ }
54
+
55
+ console.debug('Cache keys:', Object.keys(cache))
56
+
57
+ // ============================================
58
+ // CLEANUP
59
+ // ============================================
60
+
61
+ // Remove single item
62
+ cache.remove('username')
63
+
64
+ // Clear all cache
65
+ cache.removeAll()
66
+
67
+ // Or use clearAll alias
68
+ // cache.clearAll()
69
+
70
+ console.debug('Cache after clear:', cache.get('score'))
71
+
72
+ console.debug('Cache example complete!')
@@ -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
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * history.ts
3
+ *
4
+ * Scenario: an AI agent (or any code) that wants to try a batch of state
5
+ * mutations, inspect what actually changed, and only keep them if they
6
+ * look right - otherwise roll everything back in one shot.
7
+ *
8
+ * History tracking is opt-in and disabled by default to avoid overhead -
9
+ * call `memorio.enableHistory()` before you rely on undo/redo/trace.
10
+ * `snapshot()`/`diff()`/`rollback()` work independently of that flag.
11
+ */
12
+ import { memorio, state } from 'memorio'
13
+
14
+ export function tryExperiment(mutate: () => void) {
15
+ // Snapshot first - this is the safe rollback point, independent of
16
+ // whether history tracking is enabled.
17
+ const before = memorio.snapshot()
18
+
19
+ mutate()
20
+
21
+ const changes = memorio.diff(before)
22
+ console.debug('Proposed changes:', changes)
23
+
24
+ return { before, changes }
25
+ }
26
+
27
+ export function commitOrRollback(before: Record<string, unknown>, accept: boolean) {
28
+ if (!accept) {
29
+ // Full-state restore, in one step - not the same as calling undo()
30
+ // repeatedly, which only unwinds one mutation at a time.
31
+ memorio.rollback(before)
32
+ console.debug('Rolled back - state restored to pre-experiment snapshot')
33
+ }
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Undo / redo, step by step
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export function stepByStepDemo() {
41
+ memorio.enableHistory()
42
+
43
+ state.user = { name: 'Sara' }
44
+ state.counter = 100
45
+ state.items = ['a', 'b']
46
+
47
+ memorio.undo() // removes state.items
48
+ memorio.undo() // counter → undefined
49
+ memorio.redo() // counter → 100 again
50
+
51
+ console.debug('canUndo:', memorio.canUndo())
52
+ console.debug('canRedo:', memorio.canRedo())
53
+
54
+ // Keep at most 50 mutations per undo/redo stack (default: 100). Lower
55
+ // this in memory-constrained environments.
56
+ memorio.setMaxHistory(50)
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Trace log - every mutation, with before/after values and a timestamp
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export function auditTrail() {
64
+ memorio.enableHistory()
65
+
66
+ state.user.name = 'Sara'
67
+ state.counter = 1
68
+ state.counter = 2
69
+
70
+ const log = memorio.trace()
71
+ console.debug('Mutation log:', log)
72
+
73
+ // ⚠️ The trace log records every value written, including tokens/PII.
74
+ // Don't ship it as-is to a client, and clear it (or disable tracking)
75
+ // on paths that touch sensitive data.
76
+ return log
77
+ }
78
+
79
+ export function exportAndReplayTrace() {
80
+ const log = memorio.trace()
81
+ const saved = JSON.stringify(log)
82
+
83
+ // ... persist `saved` somewhere (store, a file, a request body) ...
84
+
85
+ const restored = JSON.parse(saved) as Array<{ action: string; path: string; newValue: unknown }>
86
+ for (const record of restored) {
87
+ if (record.action === 'set') {
88
+ // `state` accepts dotted-path-style top-level keys as used in the
89
+ // trace log; nested paths need a small helper to walk into `state`.
90
+ ;(state as Record<string, unknown>)[record.path] = record.newValue
91
+ }
92
+ }
93
+ }
94
+
95
+ export function resetHistory() {
96
+ memorio.clearHistory() // wipes undo/redo stacks + trace log (state itself is untouched)
97
+ memorio.clearRedo() // or just drop the redo stack, keeping undo intact
98
+ }
99
+
100
+ // Usage:
101
+ // const { before, changes } = tryExperiment(() => {
102
+ // state.user = { name: 'Sara', role: 'admin' }
103
+ // })
104
+ // commitOrRollback(before, /* accept = */ changes.length <= 3)
package/examples/idb.ts CHANGED
@@ -1,109 +1,109 @@
1
- /**
2
- * Memorio IDB Example
3
- *
4
- * This example shows how to use IndexedDB for large data storage.
5
- * IDB is perfect for caching, offline data, and large datasets.
6
- *
7
- * Run: npx ts-node examples/idb.ts
8
- */
9
-
10
- import 'memorio'
11
-
12
- // ============================================
13
- // CHECK SUPPORT
14
- // ============================================
15
-
16
- if (!idb.db.support()) {
17
- console.debug('IndexedDB not supported')
18
- process.exit(0)
19
- }
20
-
21
- // ============================================
22
- // CREATE DATABASE
23
- // ============================================
24
-
25
- // Create a new database
26
- idb.db.create('myApp')
27
-
28
- // Create tables (stores)
29
- idb.table.create('myApp', 'users')
30
- idb.table.create('myApp', 'products')
31
-
32
- // ============================================
33
- // ADD DATA
34
- // ============================================
35
-
36
- // Add user records
37
- idb.data.set('myApp', 'users', {
38
- id: 1,
39
- name: 'Mario',
40
- email: 'mario@example.com',
41
- role: 'admin'
42
- })
43
-
44
- idb.data.set('myApp', 'users', {
45
- id: 2,
46
- name: 'Luigi',
47
- email: 'luigi@example.com',
48
- role: 'user'
49
- })
50
-
51
- // Add product records
52
- idb.data.set('myApp', 'products', {
53
- id: 1,
54
- name: 'Super Mushroom',
55
- price: 99,
56
- inStock: true
57
- })
58
-
59
- idb.data.set('myApp', 'products', {
60
- id: 2,
61
- name: 'Fire Flower',
62
- price: 199,
63
- inStock: true
64
- })
65
-
66
- // ============================================
67
- // READ DATA
68
- // ============================================
69
-
70
- const user1 = idb.data.get('myApp', 'users', 1)
71
- console.debug('User 1:', user1)
72
-
73
- const product1 = idb.data.get('myApp', 'products', 1)
74
- console.debug('Product 1:', product1)
75
-
76
- // ============================================
77
- // DATABASE INFO
78
- // ============================================
79
-
80
- // List all databases
81
- const databases = idb.db.list()
82
- console.debug('Databases:', databases)
83
-
84
- // Check if database exists
85
- const exists = idb.db.exist('myApp')
86
- console.debug('myApp exists:', exists)
87
-
88
- // Get database version
89
- const version = idb.db.version('myApp')
90
- console.debug('myApp version:', version)
91
-
92
- // Get database size
93
- const size = idb.db.size('myApp')
94
- console.debug('myApp size:', size, 'bytes')
95
-
96
- // ============================================
97
- // DELETE DATA
98
- // ============================================
99
-
100
- // Delete a record
101
- idb.data.delete('myApp', 'users', 1)
102
-
103
- // Delete entire table
104
- // idb.table.delete('myApp', 'users');
105
-
106
- // Delete entire database
107
- // idb.db.delete('myApp');
108
-
109
- console.debug('IDB example complete!')
1
+ /**
2
+ * Memorio IDB Example
3
+ *
4
+ * This example shows how to use IndexedDB for large data storage.
5
+ * IDB is perfect for caching, offline data, and large datasets.
6
+ *
7
+ * Run: npx ts-node examples/idb.ts
8
+ */
9
+
10
+ import { idb } from 'memorio'
11
+
12
+ // ============================================
13
+ // CHECK SUPPORT
14
+ // ============================================
15
+
16
+ if (!idb.db.support()) {
17
+ console.debug('IndexedDB not supported')
18
+ process.exit(0)
19
+ }
20
+
21
+ // ============================================
22
+ // CREATE DATABASE
23
+ // ============================================
24
+
25
+ // Create a new database
26
+ idb.db.create('myApp')
27
+
28
+ // Create tables (stores)
29
+ idb.table.create('myApp', 'users')
30
+ idb.table.create('myApp', 'products')
31
+
32
+ // ============================================
33
+ // ADD DATA
34
+ // ============================================
35
+
36
+ // Add user records
37
+ idb.data.set('myApp', 'users', {
38
+ id: 1,
39
+ name: 'Mario',
40
+ email: 'mario@example.com',
41
+ role: 'admin'
42
+ })
43
+
44
+ idb.data.set('myApp', 'users', {
45
+ id: 2,
46
+ name: 'Luigi',
47
+ email: 'luigi@example.com',
48
+ role: 'user'
49
+ })
50
+
51
+ // Add product records
52
+ idb.data.set('myApp', 'products', {
53
+ id: 1,
54
+ name: 'Super Mushroom',
55
+ price: 99,
56
+ inStock: true
57
+ })
58
+
59
+ idb.data.set('myApp', 'products', {
60
+ id: 2,
61
+ name: 'Fire Flower',
62
+ price: 199,
63
+ inStock: true
64
+ })
65
+
66
+ // ============================================
67
+ // READ DATA
68
+ // ============================================
69
+
70
+ const user1 = idb.data.get('myApp', 'users', 1)
71
+ console.debug('User 1:', user1)
72
+
73
+ const product1 = idb.data.get('myApp', 'products', 1)
74
+ console.debug('Product 1:', product1)
75
+
76
+ // ============================================
77
+ // DATABASE INFO
78
+ // ============================================
79
+
80
+ // List all databases
81
+ const databases = idb.db.list()
82
+ console.debug('Databases:', databases)
83
+
84
+ // Check if database exists
85
+ const exists = idb.db.exist('myApp')
86
+ console.debug('myApp exists:', exists)
87
+
88
+ // Get database version
89
+ const version = idb.db.version('myApp')
90
+ console.debug('myApp version:', version)
91
+
92
+ // Get database size
93
+ const size = idb.db.size('myApp')
94
+ console.debug('myApp size:', size, 'bytes')
95
+
96
+ // ============================================
97
+ // DELETE DATA
98
+ // ============================================
99
+
100
+ // Delete a record
101
+ idb.data.delete('myApp', 'users', 1)
102
+
103
+ // Delete entire table
104
+ // idb.table.delete('myApp', 'users');
105
+
106
+ // Delete entire database
107
+ // idb.db.delete('myApp');
108
+
109
+ console.debug('IDB example complete!')
@@ -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.