@qwickapps/qwickbrain-proxy 1.0.1 → 1.0.3

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 (67) hide show
  1. package/.claude/engineering/bugs/BUG-qwickbrain-proxy-cache-and-design.md +840 -0
  2. package/.github/workflows/publish.yml +13 -0
  3. package/CHANGELOG.md +54 -0
  4. package/dist/db/schema.d.ts +63 -6
  5. package/dist/db/schema.d.ts.map +1 -1
  6. package/dist/db/schema.js +17 -2
  7. package/dist/db/schema.js.map +1 -1
  8. package/dist/lib/__tests__/cache-manager.test.js +146 -83
  9. package/dist/lib/__tests__/cache-manager.test.js.map +1 -1
  10. package/dist/lib/__tests__/proxy-server.test.js +16 -44
  11. package/dist/lib/__tests__/proxy-server.test.js.map +1 -1
  12. package/dist/lib/__tests__/sse-invalidation-listener.test.d.ts +2 -0
  13. package/dist/lib/__tests__/sse-invalidation-listener.test.d.ts.map +1 -0
  14. package/dist/lib/__tests__/sse-invalidation-listener.test.js +245 -0
  15. package/dist/lib/__tests__/sse-invalidation-listener.test.js.map +1 -0
  16. package/dist/lib/__tests__/write-queue-manager.test.d.ts +2 -0
  17. package/dist/lib/__tests__/write-queue-manager.test.d.ts.map +1 -0
  18. package/dist/lib/__tests__/write-queue-manager.test.js +291 -0
  19. package/dist/lib/__tests__/write-queue-manager.test.js.map +1 -0
  20. package/dist/lib/cache-manager.d.ts +35 -6
  21. package/dist/lib/cache-manager.d.ts.map +1 -1
  22. package/dist/lib/cache-manager.js +154 -41
  23. package/dist/lib/cache-manager.js.map +1 -1
  24. package/dist/lib/connection-manager.d.ts.map +1 -1
  25. package/dist/lib/connection-manager.js +4 -1
  26. package/dist/lib/connection-manager.js.map +1 -1
  27. package/dist/lib/proxy-server.d.ts +6 -0
  28. package/dist/lib/proxy-server.d.ts.map +1 -1
  29. package/dist/lib/proxy-server.js +182 -87
  30. package/dist/lib/proxy-server.js.map +1 -1
  31. package/dist/lib/qwickbrain-client.d.ts +4 -0
  32. package/dist/lib/qwickbrain-client.d.ts.map +1 -1
  33. package/dist/lib/qwickbrain-client.js +133 -0
  34. package/dist/lib/qwickbrain-client.js.map +1 -1
  35. package/dist/lib/sse-invalidation-listener.d.ts +27 -0
  36. package/dist/lib/sse-invalidation-listener.d.ts.map +1 -0
  37. package/dist/lib/sse-invalidation-listener.js +145 -0
  38. package/dist/lib/sse-invalidation-listener.js.map +1 -0
  39. package/dist/lib/tools.d.ts +21 -0
  40. package/dist/lib/tools.d.ts.map +1 -0
  41. package/dist/lib/tools.js +488 -0
  42. package/dist/lib/tools.js.map +1 -0
  43. package/dist/lib/write-queue-manager.d.ts +88 -0
  44. package/dist/lib/write-queue-manager.d.ts.map +1 -0
  45. package/dist/lib/write-queue-manager.js +191 -0
  46. package/dist/lib/write-queue-manager.js.map +1 -0
  47. package/dist/types/config.d.ts +7 -42
  48. package/dist/types/config.d.ts.map +1 -1
  49. package/dist/types/config.js +1 -6
  50. package/dist/types/config.js.map +1 -1
  51. package/drizzle/0002_lru_cache_migration.sql +94 -0
  52. package/drizzle/meta/_journal.json +7 -0
  53. package/package.json +6 -2
  54. package/scripts/rebuild-sqlite.sh +26 -0
  55. package/src/db/schema.ts +17 -2
  56. package/src/lib/__tests__/cache-manager.test.ts +180 -90
  57. package/src/lib/__tests__/proxy-server.test.ts +16 -51
  58. package/src/lib/__tests__/sse-invalidation-listener.test.ts +326 -0
  59. package/src/lib/__tests__/write-queue-manager.test.ts +383 -0
  60. package/src/lib/cache-manager.ts +198 -46
  61. package/src/lib/connection-manager.ts +4 -1
  62. package/src/lib/proxy-server.ts +222 -90
  63. package/src/lib/qwickbrain-client.ts +145 -1
  64. package/src/lib/sse-invalidation-listener.ts +171 -0
  65. package/src/lib/tools.ts +500 -0
  66. package/src/lib/write-queue-manager.ts +271 -0
  67. package/src/types/config.ts +1 -6
@@ -0,0 +1,271 @@
1
+ import { eq, and, or } from 'drizzle-orm';
2
+ import type { DB } from '../db/client.js';
3
+ import { syncQueue } from '../db/schema.js';
4
+ import type { QwickBrainClient } from './qwickbrain-client.js';
5
+
6
+ interface QueuedOperation {
7
+ id: number;
8
+ operation: string;
9
+ payload: string;
10
+ createdAt: Date;
11
+ status: string;
12
+ error: string | null;
13
+ attempts: number;
14
+ lastAttemptAt: Date | null;
15
+ }
16
+
17
+ interface CreateDocumentPayload {
18
+ docType: string;
19
+ name: string;
20
+ content: string;
21
+ project?: string;
22
+ metadata?: Record<string, unknown>;
23
+ }
24
+
25
+ interface SetMemoryPayload {
26
+ name: string;
27
+ content: string;
28
+ project?: string;
29
+ metadata?: Record<string, unknown>;
30
+ }
31
+
32
+ interface DeleteDocumentPayload {
33
+ docType: string;
34
+ name: string;
35
+ project?: string;
36
+ }
37
+
38
+ interface DeleteMemoryPayload {
39
+ name: string;
40
+ project?: string;
41
+ }
42
+
43
+ type OperationPayload =
44
+ | CreateDocumentPayload
45
+ | SetMemoryPayload
46
+ | DeleteDocumentPayload
47
+ | DeleteMemoryPayload;
48
+
49
+ export class WriteQueueManager {
50
+ private maxAttempts = 3;
51
+ private isSyncing = false;
52
+
53
+ constructor(
54
+ private db: DB,
55
+ private qwickbrainClient: QwickBrainClient
56
+ ) {}
57
+
58
+ /**
59
+ * Queue a write operation for later sync
60
+ */
61
+ async queueOperation(operation: string, payload: OperationPayload): Promise<void> {
62
+ await this.db.insert(syncQueue).values({
63
+ operation,
64
+ payload: JSON.stringify(payload),
65
+ status: 'pending',
66
+ attempts: 0,
67
+ });
68
+
69
+ console.error(`Queued operation: ${operation}`);
70
+ }
71
+
72
+ /**
73
+ * Get count of pending operations
74
+ */
75
+ async getPendingCount(): Promise<number> {
76
+ const result = await this.db
77
+ .select()
78
+ .from(syncQueue)
79
+ .where(eq(syncQueue.status, 'pending'));
80
+
81
+ return result.length;
82
+ }
83
+
84
+ /**
85
+ * Sync all pending operations
86
+ * Returns number of operations synced successfully
87
+ */
88
+ async syncPendingOperations(): Promise<{ synced: number; failed: number }> {
89
+ if (this.isSyncing) {
90
+ console.error('Sync already in progress, skipping');
91
+ return { synced: 0, failed: 0 };
92
+ }
93
+
94
+ this.isSyncing = true;
95
+ let synced = 0;
96
+ let failed = 0;
97
+
98
+ try {
99
+ // Get all pending operations, ordered by creation time (FIFO)
100
+ const pending = await this.db
101
+ .select()
102
+ .from(syncQueue)
103
+ .where(eq(syncQueue.status, 'pending'))
104
+ .orderBy(syncQueue.createdAt);
105
+
106
+ console.error(`Syncing ${pending.length} pending operations...`);
107
+
108
+ for (const item of pending) {
109
+ try {
110
+ await this.executeOperation(item);
111
+
112
+ // Mark as completed
113
+ await this.db
114
+ .update(syncQueue)
115
+ .set({ status: 'completed' })
116
+ .where(eq(syncQueue.id, item.id));
117
+
118
+ synced++;
119
+ console.error(`Synced operation ${item.id}: ${item.operation}`);
120
+ } catch (error) {
121
+ const errorMessage = error instanceof Error ? error.message : String(error);
122
+ const newAttempts = item.attempts + 1;
123
+
124
+ if (newAttempts >= this.maxAttempts) {
125
+ // Max attempts reached, mark as failed
126
+ await this.db
127
+ .update(syncQueue)
128
+ .set({
129
+ status: 'failed',
130
+ error: errorMessage,
131
+ attempts: newAttempts,
132
+ lastAttemptAt: new Date(),
133
+ })
134
+ .where(eq(syncQueue.id, item.id));
135
+
136
+ failed++;
137
+ console.error(`Operation ${item.id} failed after ${newAttempts} attempts: ${errorMessage}`);
138
+ } else {
139
+ // Increment attempts, keep as pending
140
+ await this.db
141
+ .update(syncQueue)
142
+ .set({
143
+ attempts: newAttempts,
144
+ lastAttemptAt: new Date(),
145
+ error: errorMessage,
146
+ })
147
+ .where(eq(syncQueue.id, item.id));
148
+
149
+ console.error(`Operation ${item.id} failed (attempt ${newAttempts}/${this.maxAttempts}): ${errorMessage}`);
150
+ }
151
+ }
152
+ }
153
+
154
+ // Clean up completed operations (keep failed ones for inspection)
155
+ await this.cleanupCompleted();
156
+
157
+ console.error(`Sync complete: ${synced} synced, ${failed} failed`);
158
+ return { synced, failed };
159
+ } finally {
160
+ this.isSyncing = false;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Execute a single queued operation
166
+ */
167
+ private async executeOperation(item: QueuedOperation): Promise<void> {
168
+ const payload = JSON.parse(item.payload);
169
+
170
+ switch (item.operation) {
171
+ case 'create_document':
172
+ case 'update_document': {
173
+ const p = payload as CreateDocumentPayload;
174
+ await this.qwickbrainClient.createDocument(
175
+ p.docType,
176
+ p.name,
177
+ p.content,
178
+ p.project,
179
+ p.metadata
180
+ );
181
+ break;
182
+ }
183
+
184
+ case 'set_memory':
185
+ case 'update_memory': {
186
+ const p = payload as SetMemoryPayload;
187
+ await this.qwickbrainClient.setMemory(p.name, p.content, p.project, p.metadata);
188
+ break;
189
+ }
190
+
191
+ case 'delete_document': {
192
+ const p = payload as DeleteDocumentPayload;
193
+ await this.qwickbrainClient.deleteDocument(p.docType, p.name, p.project);
194
+ break;
195
+ }
196
+
197
+ case 'delete_memory': {
198
+ const p = payload as DeleteMemoryPayload;
199
+ await this.qwickbrainClient.deleteMemory(p.name, p.project);
200
+ break;
201
+ }
202
+
203
+ default:
204
+ throw new Error(`Unknown operation: ${item.operation}`);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Clean up completed operations
210
+ */
211
+ private async cleanupCompleted(): Promise<void> {
212
+ await this.db.delete(syncQueue).where(eq(syncQueue.status, 'completed'));
213
+ }
214
+
215
+ /**
216
+ * Get failed operations for inspection
217
+ */
218
+ async getFailedOperations(): Promise<QueuedOperation[]> {
219
+ return await this.db
220
+ .select()
221
+ .from(syncQueue)
222
+ .where(eq(syncQueue.status, 'failed'))
223
+ .orderBy(syncQueue.createdAt);
224
+ }
225
+
226
+ /**
227
+ * Retry a specific failed operation
228
+ */
229
+ async retryOperation(id: number): Promise<void> {
230
+ await this.db
231
+ .update(syncQueue)
232
+ .set({
233
+ status: 'pending',
234
+ attempts: 0,
235
+ error: null,
236
+ lastAttemptAt: null,
237
+ })
238
+ .where(eq(syncQueue.id, id));
239
+
240
+ console.error(`Operation ${id} reset to pending for retry`);
241
+ }
242
+
243
+ /**
244
+ * Clear all failed operations
245
+ */
246
+ async clearFailed(): Promise<number> {
247
+ const failed = await this.getFailedOperations();
248
+ await this.db.delete(syncQueue).where(eq(syncQueue.status, 'failed'));
249
+ console.error(`Cleared ${failed.length} failed operations`);
250
+ return failed.length;
251
+ }
252
+
253
+ /**
254
+ * Get queue statistics
255
+ */
256
+ async getQueueStats(): Promise<{
257
+ pending: number;
258
+ failed: number;
259
+ total: number;
260
+ }> {
261
+ const all = await this.db.select().from(syncQueue);
262
+ const pending = all.filter(item => item.status === 'pending').length;
263
+ const failed = all.filter(item => item.status === 'failed').length;
264
+
265
+ return {
266
+ pending,
267
+ failed,
268
+ total: all.length,
269
+ };
270
+ }
271
+ }
@@ -12,12 +12,7 @@ export const ConfigSchema = z.object({
12
12
  }).default({}),
13
13
  cache: z.object({
14
14
  dir: z.string().optional(),
15
- ttl: z.object({
16
- workflows: z.number().default(86400), // 24 hours
17
- rules: z.number().default(86400), // 24 hours
18
- documents: z.number().default(21600), // 6 hours
19
- memories: z.number().default(3600), // 1 hour
20
- }).default({}),
15
+ maxCacheSizeBytes: z.number().default(100 * 1024 * 1024), // 100MB default for dynamic tier
21
16
  preload: z.array(z.string()).default(['workflows', 'rules']),
22
17
  }).default({}),
23
18
  connection: z.object({