family-ai-agent 1.0.6 → 1.0.7
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/.letta/settings.local.json +3 -0
- package/dist/database/adapters/base-adapter.d.ts +81 -0
- package/dist/database/adapters/base-adapter.d.ts.map +1 -0
- package/dist/database/adapters/base-adapter.js +105 -0
- package/dist/database/adapters/base-adapter.js.map +1 -0
- package/dist/database/adapters/index.d.ts +49 -0
- package/dist/database/adapters/index.d.ts.map +1 -0
- package/dist/database/adapters/index.js +200 -0
- package/dist/database/adapters/index.js.map +1 -0
- package/dist/database/adapters/postgres-adapter.d.ts +75 -0
- package/dist/database/adapters/postgres-adapter.d.ts.map +1 -0
- package/dist/database/adapters/postgres-adapter.js +225 -0
- package/dist/database/adapters/postgres-adapter.js.map +1 -0
- package/dist/database/adapters/sqlite-adapter.d.ts +72 -0
- package/dist/database/adapters/sqlite-adapter.d.ts.map +1 -0
- package/dist/database/adapters/sqlite-adapter.js +368 -0
- package/dist/database/adapters/sqlite-adapter.js.map +1 -0
- package/dist/database/cache/cache-keys.d.ts +180 -0
- package/dist/database/cache/cache-keys.d.ts.map +1 -0
- package/dist/database/cache/cache-keys.js +107 -0
- package/dist/database/cache/cache-keys.js.map +1 -0
- package/dist/database/cache/index.d.ts +24 -0
- package/dist/database/cache/index.d.ts.map +1 -0
- package/dist/database/cache/index.js +34 -0
- package/dist/database/cache/index.js.map +1 -0
- package/dist/database/cache/query-cache.d.ts +67 -0
- package/dist/database/cache/query-cache.d.ts.map +1 -0
- package/dist/database/cache/query-cache.js +177 -0
- package/dist/database/cache/query-cache.js.map +1 -0
- package/dist/database/client.d.ts +63 -4
- package/dist/database/client.d.ts.map +1 -1
- package/dist/database/client.js +147 -59
- package/dist/database/client.js.map +1 -1
- package/dist/database/db-config.d.ts +104 -0
- package/dist/database/db-config.d.ts.map +1 -0
- package/dist/database/db-config.js +167 -0
- package/dist/database/db-config.js.map +1 -0
- package/dist/database/drizzle/index.d.ts +42 -0
- package/dist/database/drizzle/index.d.ts.map +1 -0
- package/dist/database/drizzle/index.js +48 -0
- package/dist/database/drizzle/index.js.map +1 -0
- package/dist/database/drizzle/schema/audit.d.ts +533 -0
- package/dist/database/drizzle/schema/audit.d.ts.map +1 -0
- package/dist/database/drizzle/schema/audit.js +71 -0
- package/dist/database/drizzle/schema/audit.js.map +1 -0
- package/dist/database/drizzle/schema/checkpoints.d.ts +665 -0
- package/dist/database/drizzle/schema/checkpoints.d.ts.map +1 -0
- package/dist/database/drizzle/schema/checkpoints.js +110 -0
- package/dist/database/drizzle/schema/checkpoints.js.map +1 -0
- package/dist/database/drizzle/schema/conversations.d.ts +449 -0
- package/dist/database/drizzle/schema/conversations.d.ts.map +1 -0
- package/dist/database/drizzle/schema/conversations.js +91 -0
- package/dist/database/drizzle/schema/conversations.js.map +1 -0
- package/dist/database/drizzle/schema/documents.d.ts +600 -0
- package/dist/database/drizzle/schema/documents.d.ts.map +1 -0
- package/dist/database/drizzle/schema/documents.js +100 -0
- package/dist/database/drizzle/schema/documents.js.map +1 -0
- package/dist/database/drizzle/schema/index.d.ts +3084 -0
- package/dist/database/drizzle/schema/index.d.ts.map +1 -0
- package/dist/database/drizzle/schema/index.js +46 -0
- package/dist/database/drizzle/schema/index.js.map +1 -0
- package/dist/database/drizzle/schema/memories.d.ts +435 -0
- package/dist/database/drizzle/schema/memories.d.ts.map +1 -0
- package/dist/database/drizzle/schema/memories.js +73 -0
- package/dist/database/drizzle/schema/memories.js.map +1 -0
- package/dist/database/drizzle/schema/tasks.d.ts +565 -0
- package/dist/database/drizzle/schema/tasks.d.ts.map +1 -0
- package/dist/database/drizzle/schema/tasks.js +84 -0
- package/dist/database/drizzle/schema/tasks.js.map +1 -0
- package/dist/database/health/circuit-breaker.d.ts +81 -0
- package/dist/database/health/circuit-breaker.d.ts.map +1 -0
- package/dist/database/health/circuit-breaker.js +184 -0
- package/dist/database/health/circuit-breaker.js.map +1 -0
- package/dist/database/health/health-monitor.d.ts +69 -0
- package/dist/database/health/health-monitor.d.ts.map +1 -0
- package/dist/database/health/health-monitor.js +174 -0
- package/dist/database/health/health-monitor.js.map +1 -0
- package/dist/database/health/index.d.ts +27 -0
- package/dist/database/health/index.d.ts.map +1 -0
- package/dist/database/health/index.js +23 -0
- package/dist/database/health/index.js.map +1 -0
- package/dist/database/index.d.ts +16 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +41 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/migrations/index.d.ts +34 -0
- package/dist/database/migrations/index.d.ts.map +1 -0
- package/dist/database/migrations/index.js +45 -0
- package/dist/database/migrations/index.js.map +1 -0
- package/dist/database/migrations/migrator.d.ts +77 -0
- package/dist/database/migrations/migrator.d.ts.map +1 -0
- package/dist/database/migrations/migrator.js +258 -0
- package/dist/database/migrations/migrator.js.map +1 -0
- package/dist/database/migrations/versions/001_initial.d.ts +9 -0
- package/dist/database/migrations/versions/001_initial.d.ts.map +1 -0
- package/dist/database/migrations/versions/001_initial.js +183 -0
- package/dist/database/migrations/versions/001_initial.js.map +1 -0
- package/dist/database/types.d.ts +255 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +8 -0
- package/dist/database/types.js.map +1 -0
- package/dist/database/vector/embedding-cache.d.ts +92 -0
- package/dist/database/vector/embedding-cache.d.ts.map +1 -0
- package/dist/database/vector/embedding-cache.js +185 -0
- package/dist/database/vector/embedding-cache.js.map +1 -0
- package/dist/database/vector/hnsw-index.d.ts +111 -0
- package/dist/database/vector/hnsw-index.d.ts.map +1 -0
- package/dist/database/vector/hnsw-index.js +337 -0
- package/dist/database/vector/hnsw-index.js.map +1 -0
- package/dist/database/vector/index.d.ts +75 -0
- package/dist/database/vector/index.d.ts.map +1 -0
- package/dist/database/vector/index.js +213 -0
- package/dist/database/vector/index.js.map +1 -0
- package/dist/database/vector/similarity.d.ts +67 -0
- package/dist/database/vector/similarity.d.ts.map +1 -0
- package/dist/database/vector/similarity.js +176 -0
- package/dist/database/vector/similarity.js.map +1 -0
- package/package.json +6 -3
- package/src/database/adapters/base-adapter.ts +171 -0
- package/src/database/adapters/index.ts +224 -0
- package/src/database/adapters/postgres-adapter.ts +285 -0
- package/src/database/adapters/sqlite-adapter.ts +420 -0
- package/src/database/cache/cache-keys.ts +150 -0
- package/src/database/cache/index.ts +44 -0
- package/src/database/cache/query-cache.ts +213 -0
- package/src/database/client.ts +166 -64
- package/src/database/db-config.ts +194 -0
- package/src/database/drizzle/index.ts +66 -0
- package/src/database/drizzle/schema/audit.ts +127 -0
- package/src/database/drizzle/schema/checkpoints.ts +164 -0
- package/src/database/drizzle/schema/conversations.ts +138 -0
- package/src/database/drizzle/schema/documents.ts +157 -0
- package/src/database/drizzle/schema/index.ts +139 -0
- package/src/database/drizzle/schema/memories.ts +127 -0
- package/src/database/drizzle/schema/tasks.ts +129 -0
- package/src/database/health/circuit-breaker.ts +214 -0
- package/src/database/health/health-monitor.ts +224 -0
- package/src/database/health/index.ts +41 -0
- package/src/database/index.ts +157 -0
- package/src/database/migrations/index.ts +52 -0
- package/src/database/migrations/migrator.ts +325 -0
- package/src/database/migrations/versions/001_initial.ts +198 -0
- package/src/database/types.ts +324 -0
- package/src/database/vector/embedding-cache.ts +234 -0
- package/src/database/vector/hnsw-index.ts +452 -0
- package/src/database/vector/index.ts +292 -0
- package/src/database/vector/similarity.ts +198 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Key Generation
|
|
3
|
+
*
|
|
4
|
+
* Utilities for generating consistent cache keys.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate a cache key from operation, table, and parameters
|
|
9
|
+
*/
|
|
10
|
+
export function generateCacheKey(
|
|
11
|
+
operation: string,
|
|
12
|
+
table: string,
|
|
13
|
+
params: Record<string, unknown>
|
|
14
|
+
): string {
|
|
15
|
+
const sortedParams = Object.keys(params)
|
|
16
|
+
.sort()
|
|
17
|
+
.map((key) => `${key}:${JSON.stringify(params[key])}`)
|
|
18
|
+
.join('|');
|
|
19
|
+
|
|
20
|
+
return `${operation}:${table}:${sortedParams}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cache key generators for common operations
|
|
25
|
+
*/
|
|
26
|
+
export const cacheKeys = {
|
|
27
|
+
/**
|
|
28
|
+
* Get conversation by thread ID
|
|
29
|
+
*/
|
|
30
|
+
conversation: (threadId: string): string =>
|
|
31
|
+
generateCacheKey('get', 'conversations', { threadId }),
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get conversation by ID
|
|
35
|
+
*/
|
|
36
|
+
conversationById: (id: string): string =>
|
|
37
|
+
generateCacheKey('get', 'conversations', { id }),
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* List messages for a conversation
|
|
41
|
+
*/
|
|
42
|
+
messages: (conversationId: string, limit: number, offset: number): string =>
|
|
43
|
+
generateCacheKey('list', 'messages', { conversationId, limit, offset }),
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get document by ID
|
|
47
|
+
*/
|
|
48
|
+
document: (id: string): string =>
|
|
49
|
+
generateCacheKey('get', 'documents', { id }),
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* List documents for a user
|
|
53
|
+
*/
|
|
54
|
+
userDocuments: (userId: string, limit: number, offset: number): string =>
|
|
55
|
+
generateCacheKey('list', 'documents', { userId, limit, offset }),
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get memory by ID
|
|
59
|
+
*/
|
|
60
|
+
memory: (id: string): string =>
|
|
61
|
+
generateCacheKey('get', 'long_term_memories', { id }),
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List recent memories for a user
|
|
65
|
+
*/
|
|
66
|
+
userMemories: (userId: string, limit: number): string =>
|
|
67
|
+
generateCacheKey('list', 'long_term_memories', { userId, limit }),
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Vector search results
|
|
71
|
+
*/
|
|
72
|
+
vectorSearch: (
|
|
73
|
+
embeddingHash: string,
|
|
74
|
+
options: Record<string, unknown>
|
|
75
|
+
): string =>
|
|
76
|
+
generateCacheKey('search', 'embeddings', { hash: embeddingHash, ...options }),
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Task by ID
|
|
80
|
+
*/
|
|
81
|
+
task: (id: string): string =>
|
|
82
|
+
generateCacheKey('get', 'tasks', { id }),
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* List tasks for a user
|
|
86
|
+
*/
|
|
87
|
+
userTasks: (userId: string, status: string, limit: number): string =>
|
|
88
|
+
generateCacheKey('list', 'tasks', { userId, status, limit }),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Invalidation patterns for write operations
|
|
93
|
+
*/
|
|
94
|
+
export const invalidationPatterns = {
|
|
95
|
+
/**
|
|
96
|
+
* Invalidate all conversation-related cache
|
|
97
|
+
*/
|
|
98
|
+
conversation: (conversationId: string): RegExp =>
|
|
99
|
+
new RegExp(`conversations.*${conversationId}`),
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Invalidate all message-related cache for a conversation
|
|
103
|
+
*/
|
|
104
|
+
messages: (conversationId: string): RegExp =>
|
|
105
|
+
new RegExp(`messages.*${conversationId}`),
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Invalidate all document-related cache
|
|
109
|
+
*/
|
|
110
|
+
document: (documentId: string): RegExp =>
|
|
111
|
+
new RegExp(`documents.*${documentId}`),
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Invalidate user's document list
|
|
115
|
+
*/
|
|
116
|
+
userDocuments: (userId: string): string => `list:documents:userId:"${userId}"`,
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Invalidate all memory-related cache
|
|
120
|
+
*/
|
|
121
|
+
memory: (memoryId: string): RegExp =>
|
|
122
|
+
new RegExp(`long_term_memories.*${memoryId}`),
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Invalidate user's memory list
|
|
126
|
+
*/
|
|
127
|
+
userMemories: (userId: string): string =>
|
|
128
|
+
`list:long_term_memories:userId:"${userId}"`,
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Invalidate all vector search results
|
|
132
|
+
*/
|
|
133
|
+
vectorSearchAll: (): string => 'search:embeddings',
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Invalidate all task-related cache
|
|
137
|
+
*/
|
|
138
|
+
task: (taskId: string): RegExp => new RegExp(`tasks.*${taskId}`),
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Invalidate user's task list
|
|
142
|
+
*/
|
|
143
|
+
userTasks: (userId: string): string => `list:tasks:userId:"${userId}"`,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export default {
|
|
147
|
+
generateCacheKey,
|
|
148
|
+
cacheKeys,
|
|
149
|
+
invalidationPatterns,
|
|
150
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Layer Exports
|
|
3
|
+
*
|
|
4
|
+
* Central export for query caching functionality.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { QueryCache } from './query-cache.js';
|
|
8
|
+
export {
|
|
9
|
+
generateCacheKey,
|
|
10
|
+
cacheKeys,
|
|
11
|
+
invalidationPatterns,
|
|
12
|
+
} from './cache-keys.js';
|
|
13
|
+
|
|
14
|
+
import { QueryCache } from './query-cache.js';
|
|
15
|
+
import type { QueryCacheOptions } from '../types.js';
|
|
16
|
+
|
|
17
|
+
// Singleton cache instance
|
|
18
|
+
let cacheInstance: QueryCache | null = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the global query cache instance
|
|
22
|
+
*/
|
|
23
|
+
export function getQueryCache(options?: Partial<QueryCacheOptions>): QueryCache {
|
|
24
|
+
if (!cacheInstance) {
|
|
25
|
+
cacheInstance = new QueryCache(options);
|
|
26
|
+
}
|
|
27
|
+
return cacheInstance;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Reset the global cache instance
|
|
32
|
+
*/
|
|
33
|
+
export function resetQueryCache(): void {
|
|
34
|
+
if (cacheInstance) {
|
|
35
|
+
cacheInstance.clear();
|
|
36
|
+
cacheInstance = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
QueryCache,
|
|
42
|
+
getQueryCache,
|
|
43
|
+
resetQueryCache,
|
|
44
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Cache
|
|
3
|
+
*
|
|
4
|
+
* LRU cache for database query results.
|
|
5
|
+
* Improves performance by avoiding redundant queries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CacheStats, CacheEntry, QueryCacheOptions } from '../types.js';
|
|
9
|
+
import { createLogger } from '../../utils/logger.js';
|
|
10
|
+
|
|
11
|
+
const logger = createLogger('QueryCache');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Default cache options
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_OPTIONS: QueryCacheOptions = {
|
|
17
|
+
maxSize: 1000,
|
|
18
|
+
defaultTtlMs: 300000, // 5 minutes
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* LRU query cache
|
|
23
|
+
*/
|
|
24
|
+
export class QueryCache {
|
|
25
|
+
private cache: Map<string, CacheEntry<unknown>> = new Map();
|
|
26
|
+
private options: QueryCacheOptions;
|
|
27
|
+
private hitCount: number = 0;
|
|
28
|
+
private missCount: number = 0;
|
|
29
|
+
|
|
30
|
+
constructor(options: Partial<QueryCacheOptions> = {}) {
|
|
31
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get value from cache
|
|
36
|
+
*/
|
|
37
|
+
get<T>(key: string): T | undefined {
|
|
38
|
+
const entry = this.cache.get(key);
|
|
39
|
+
|
|
40
|
+
if (!entry) {
|
|
41
|
+
this.missCount++;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check expiration
|
|
46
|
+
if (Date.now() > entry.expiresAt) {
|
|
47
|
+
this.cache.delete(key);
|
|
48
|
+
this.missCount++;
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Update access metadata and move to end for LRU
|
|
53
|
+
entry.hits++;
|
|
54
|
+
this.cache.delete(key);
|
|
55
|
+
this.cache.set(key, entry);
|
|
56
|
+
|
|
57
|
+
this.hitCount++;
|
|
58
|
+
return entry.value as T;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set value in cache
|
|
63
|
+
*/
|
|
64
|
+
set<T>(key: string, value: T, ttlMs?: number): void {
|
|
65
|
+
// Evict oldest if at capacity
|
|
66
|
+
if (this.cache.size >= this.options.maxSize && !this.cache.has(key)) {
|
|
67
|
+
this.evictLRU();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const entry: CacheEntry<T> = {
|
|
71
|
+
value,
|
|
72
|
+
expiresAt: Date.now() + (ttlMs ?? this.options.defaultTtlMs),
|
|
73
|
+
hits: 0,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Delete first to ensure it's at the end
|
|
77
|
+
this.cache.delete(key);
|
|
78
|
+
this.cache.set(key, entry);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if key exists (without updating access)
|
|
83
|
+
*/
|
|
84
|
+
has(key: string): boolean {
|
|
85
|
+
const entry = this.cache.get(key);
|
|
86
|
+
if (!entry) return false;
|
|
87
|
+
if (Date.now() > entry.expiresAt) {
|
|
88
|
+
this.cache.delete(key);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Delete key from cache
|
|
96
|
+
*/
|
|
97
|
+
delete(key: string): boolean {
|
|
98
|
+
return this.cache.delete(key);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Invalidate entries matching a pattern
|
|
103
|
+
*/
|
|
104
|
+
invalidate(pattern: string | RegExp): number {
|
|
105
|
+
let count = 0;
|
|
106
|
+
for (const key of this.cache.keys()) {
|
|
107
|
+
const matches =
|
|
108
|
+
typeof pattern === 'string'
|
|
109
|
+
? key.includes(pattern)
|
|
110
|
+
: pattern.test(key);
|
|
111
|
+
|
|
112
|
+
if (matches) {
|
|
113
|
+
this.cache.delete(key);
|
|
114
|
+
count++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (count > 0) {
|
|
119
|
+
logger.debug('Cache invalidated', { pattern: String(pattern), count });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return count;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clear all entries
|
|
127
|
+
*/
|
|
128
|
+
clear(): void {
|
|
129
|
+
this.cache.clear();
|
|
130
|
+
logger.debug('Cache cleared');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Evict least recently used entry
|
|
135
|
+
*/
|
|
136
|
+
private evictLRU(): void {
|
|
137
|
+
const firstKey = this.cache.keys().next().value;
|
|
138
|
+
if (firstKey) {
|
|
139
|
+
this.cache.delete(firstKey);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Remove expired entries
|
|
145
|
+
*/
|
|
146
|
+
prune(): number {
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
let count = 0;
|
|
149
|
+
|
|
150
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
151
|
+
if (now > entry.expiresAt) {
|
|
152
|
+
this.cache.delete(key);
|
|
153
|
+
count++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (count > 0) {
|
|
158
|
+
logger.debug('Cache pruned', { removed: count });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return count;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get cache statistics
|
|
166
|
+
*/
|
|
167
|
+
getStats(): CacheStats {
|
|
168
|
+
const totalRequests = this.hitCount + this.missCount;
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
size: this.cache.size,
|
|
172
|
+
maxSize: this.options.maxSize,
|
|
173
|
+
hitCount: this.hitCount,
|
|
174
|
+
missCount: this.missCount,
|
|
175
|
+
hitRate: totalRequests > 0 ? this.hitCount / totalRequests : 0,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get cache size
|
|
181
|
+
*/
|
|
182
|
+
get size(): number {
|
|
183
|
+
return this.cache.size;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Reset statistics
|
|
188
|
+
*/
|
|
189
|
+
resetStats(): void {
|
|
190
|
+
this.hitCount = 0;
|
|
191
|
+
this.missCount = 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get or set pattern - fetch from cache or execute function and cache result
|
|
196
|
+
*/
|
|
197
|
+
async getOrSet<T>(
|
|
198
|
+
key: string,
|
|
199
|
+
fn: () => Promise<T>,
|
|
200
|
+
ttlMs?: number
|
|
201
|
+
): Promise<T> {
|
|
202
|
+
const cached = this.get<T>(key);
|
|
203
|
+
if (cached !== undefined) {
|
|
204
|
+
return cached;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const result = await fn();
|
|
208
|
+
this.set(key, result, ttlMs);
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default QueryCache;
|
package/src/database/client.ts
CHANGED
|
@@ -1,103 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Client
|
|
3
|
+
*
|
|
4
|
+
* Unified database client with backward-compatible API.
|
|
5
|
+
* Automatically detects and uses PostgreSQL or SQLite.
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import pg from 'pg';
|
|
2
|
-
import {
|
|
9
|
+
import { getAdapter, closeAdapter, isAdapterInitialized, getAdapterType } from './adapters/index.js';
|
|
10
|
+
import { createMigrator } from './migrations/index.js';
|
|
11
|
+
import { createHealthSystem } from './health/index.js';
|
|
12
|
+
import { getDatabaseConfig } from './db-config.js';
|
|
13
|
+
import type { DatabaseAdapter, QueryResult, TransactionClient } from './types.js';
|
|
3
14
|
import { createLogger } from '../utils/logger.js';
|
|
4
15
|
import { MemoryError } from '../utils/errors.js';
|
|
5
16
|
|
|
6
|
-
const { Pool } = pg;
|
|
7
17
|
const logger = createLogger('Database');
|
|
8
18
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export function getPool(): pg.Pool {
|
|
12
|
-
if (!pool) {
|
|
13
|
-
pool = new Pool({
|
|
14
|
-
connectionString: getDatabaseUrl(),
|
|
15
|
-
max: 20,
|
|
16
|
-
idleTimeoutMillis: 30000,
|
|
17
|
-
connectionTimeoutMillis: 5000,
|
|
18
|
-
});
|
|
19
|
+
// Health system instance
|
|
20
|
+
let healthSystem: ReturnType<typeof createHealthSystem> | null = null;
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Get the current database adapter
|
|
24
|
+
*/
|
|
25
|
+
async function getAdapterInstance(): Promise<DatabaseAdapter> {
|
|
26
|
+
return getAdapter();
|
|
27
|
+
}
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Get PostgreSQL pool (for backward compatibility)
|
|
31
|
+
* Returns null if using SQLite
|
|
32
|
+
*/
|
|
33
|
+
export function getPool(): pg.Pool | null {
|
|
34
|
+
if (getAdapterType() === 'postgresql') {
|
|
35
|
+
// Access the raw pool from PostgreSQL adapter
|
|
36
|
+
try {
|
|
37
|
+
const adapter = require('./adapters/postgres-adapter.js').PostgresAdapter;
|
|
38
|
+
// This is a simplified compatibility layer
|
|
39
|
+
return null; // Actual pool access requires async initialization
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
27
43
|
}
|
|
28
|
-
|
|
29
|
-
return pool;
|
|
44
|
+
return null;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Execute a database query
|
|
49
|
+
* Compatible with both PostgreSQL and SQLite
|
|
50
|
+
*/
|
|
51
|
+
export async function query<T extends Record<string, any> = Record<string, any>>(
|
|
33
52
|
text: string,
|
|
34
53
|
params?: unknown[]
|
|
35
|
-
): Promise<
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const result = await pool.query<T>(text, params);
|
|
41
|
-
const duration = Date.now() - start;
|
|
42
|
-
logger.debug('Query executed', { duration, rows: result.rowCount });
|
|
43
|
-
return result;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
46
|
-
logger.error('Query failed', { error: errorMessage, query: text.slice(0, 100) });
|
|
47
|
-
throw new MemoryError(`Database query failed: ${errorMessage}`);
|
|
48
|
-
}
|
|
54
|
+
): Promise<QueryResult<T>> {
|
|
55
|
+
const adapter = await getAdapterInstance();
|
|
56
|
+
return adapter.query<T>(text, params);
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Get a transaction client
|
|
61
|
+
* For PostgreSQL: returns PoolClient
|
|
62
|
+
* For SQLite: returns a wrapper
|
|
63
|
+
*/
|
|
64
|
+
export async function getClient(): Promise<TransactionClient> {
|
|
65
|
+
const adapter = await getAdapterInstance();
|
|
66
|
+
return {
|
|
67
|
+
query: async <T extends Record<string, any> = Record<string, any>>(
|
|
68
|
+
sql: string,
|
|
69
|
+
params?: unknown[]
|
|
70
|
+
): Promise<QueryResult<T>> => {
|
|
71
|
+
return adapter.query<T>(sql, params);
|
|
72
|
+
},
|
|
73
|
+
};
|
|
54
74
|
}
|
|
55
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Execute a transaction
|
|
78
|
+
*/
|
|
56
79
|
export async function transaction<T>(
|
|
57
|
-
callback: (client:
|
|
80
|
+
callback: (client: TransactionClient) => Promise<T>
|
|
58
81
|
): Promise<T> {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
await client.query('BEGIN');
|
|
63
|
-
const result = await callback(client);
|
|
64
|
-
await client.query('COMMIT');
|
|
65
|
-
return result;
|
|
66
|
-
} catch (error) {
|
|
67
|
-
await client.query('ROLLBACK');
|
|
68
|
-
throw error;
|
|
69
|
-
} finally {
|
|
70
|
-
client.release();
|
|
71
|
-
}
|
|
82
|
+
const adapter = await getAdapterInstance();
|
|
83
|
+
return adapter.transaction(callback);
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Check database health
|
|
88
|
+
*/
|
|
74
89
|
export async function healthCheck(): Promise<boolean> {
|
|
75
90
|
try {
|
|
76
|
-
const
|
|
77
|
-
return
|
|
91
|
+
const adapter = await getAdapterInstance();
|
|
92
|
+
return adapter.healthCheck();
|
|
78
93
|
} catch {
|
|
79
94
|
return false;
|
|
80
95
|
}
|
|
81
96
|
}
|
|
82
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Close the database connection
|
|
100
|
+
*/
|
|
83
101
|
export async function closePool(): Promise<void> {
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
logger.info('Database pool closed');
|
|
102
|
+
if (healthSystem) {
|
|
103
|
+
healthSystem.healthMonitor.stop();
|
|
104
|
+
healthSystem = null;
|
|
88
105
|
}
|
|
106
|
+
await closeAdapter();
|
|
107
|
+
logger.info('Database connection closed');
|
|
89
108
|
}
|
|
90
109
|
|
|
91
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Initialize the database
|
|
112
|
+
* Performs auto-detection, runs migrations, and starts health monitoring
|
|
113
|
+
*/
|
|
92
114
|
export async function initDatabase(): Promise<void> {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
try {
|
|
116
|
+
const adapter = await getAdapterInstance();
|
|
117
|
+
const config = getDatabaseConfig();
|
|
118
|
+
|
|
119
|
+
logger.info('Database initialized', {
|
|
120
|
+
type: adapter.type,
|
|
121
|
+
path: adapter.type === 'sqlite' ? config.sqlitePath : undefined,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Run migrations
|
|
125
|
+
const migrator = createMigrator(adapter);
|
|
126
|
+
const pending = await migrator.getPendingMigrations();
|
|
127
|
+
|
|
128
|
+
if (pending.length > 0) {
|
|
129
|
+
logger.info('Running pending migrations', { count: pending.length });
|
|
130
|
+
const results = await migrator.migrate();
|
|
131
|
+
const failed = results.filter((r) => !r.success);
|
|
132
|
+
if (failed.length > 0) {
|
|
133
|
+
logger.error('Some migrations failed', { failed });
|
|
134
|
+
throw new MemoryError(`Migration failed: ${failed[0]?.error}`);
|
|
135
|
+
}
|
|
136
|
+
logger.info('Migrations completed', { count: results.length });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Start health monitoring
|
|
140
|
+
healthSystem = createHealthSystem(adapter, {
|
|
141
|
+
circuitBreaker: {
|
|
142
|
+
failureThreshold: config.circuitBreakerThreshold,
|
|
143
|
+
resetTimeoutMs: config.circuitBreakerTimeoutMs,
|
|
144
|
+
},
|
|
145
|
+
healthMonitor: {
|
|
146
|
+
checkIntervalMs: config.healthCheckIntervalMs,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
healthSystem.healthMonitor.start();
|
|
150
|
+
|
|
151
|
+
// Verify connection
|
|
152
|
+
const healthy = await adapter.healthCheck();
|
|
153
|
+
if (!healthy) {
|
|
154
|
+
throw new MemoryError('Database health check failed after initialization');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
logger.info('Database ready', { type: adapter.type });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
160
|
+
logger.error('Database initialization failed', { error: message });
|
|
161
|
+
throw new MemoryError(`Failed to initialize database: ${message}`);
|
|
98
162
|
}
|
|
99
163
|
}
|
|
100
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Get database type
|
|
167
|
+
*/
|
|
168
|
+
export function getDatabaseType(): 'postgresql' | 'sqlite' | null {
|
|
169
|
+
return getAdapterType();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if database is initialized
|
|
174
|
+
*/
|
|
175
|
+
export function isDatabaseInitialized(): boolean {
|
|
176
|
+
return isAdapterInitialized();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get health status
|
|
181
|
+
*/
|
|
182
|
+
export function getHealthStatus() {
|
|
183
|
+
return healthSystem?.healthMonitor.getStatus() ?? null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get health summary
|
|
188
|
+
*/
|
|
189
|
+
export function getHealthSummary() {
|
|
190
|
+
return healthSystem?.healthMonitor.getSummary() ?? {
|
|
191
|
+
healthy: false,
|
|
192
|
+
status: 'unknown',
|
|
193
|
+
database: 'unknown',
|
|
194
|
+
latencyMs: -1,
|
|
195
|
+
circuitBreaker: 'unknown',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
101
199
|
export default {
|
|
102
200
|
getPool,
|
|
103
201
|
query,
|
|
@@ -106,4 +204,8 @@ export default {
|
|
|
106
204
|
healthCheck,
|
|
107
205
|
closePool,
|
|
108
206
|
initDatabase,
|
|
207
|
+
getDatabaseType,
|
|
208
|
+
isDatabaseInitialized,
|
|
209
|
+
getHealthStatus,
|
|
210
|
+
getHealthSummary,
|
|
109
211
|
};
|