@qwickapps/qwickbrain-proxy 1.0.2 → 1.1.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.
- package/CHANGELOG.md +17 -0
- package/dist/db/schema.d.ts +63 -6
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +17 -2
- package/dist/db/schema.js.map +1 -1
- package/dist/lib/__tests__/cache-manager.test.js +146 -83
- package/dist/lib/__tests__/cache-manager.test.js.map +1 -1
- package/dist/lib/__tests__/connection-manager.test.js +2 -2
- package/dist/lib/__tests__/connection-manager.test.js.map +1 -1
- package/dist/lib/__tests__/proxy-server.test.js +16 -44
- package/dist/lib/__tests__/proxy-server.test.js.map +1 -1
- package/dist/lib/__tests__/qwickbrain-client.test.js +3 -1
- package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -1
- package/dist/lib/__tests__/sse-invalidation-listener.test.d.ts +2 -0
- package/dist/lib/__tests__/sse-invalidation-listener.test.d.ts.map +1 -0
- package/dist/lib/__tests__/sse-invalidation-listener.test.js +245 -0
- package/dist/lib/__tests__/sse-invalidation-listener.test.js.map +1 -0
- package/dist/lib/__tests__/write-queue-manager.test.d.ts +2 -0
- package/dist/lib/__tests__/write-queue-manager.test.d.ts.map +1 -0
- package/dist/lib/__tests__/write-queue-manager.test.js +291 -0
- package/dist/lib/__tests__/write-queue-manager.test.js.map +1 -0
- package/dist/lib/cache-manager.d.ts +35 -6
- package/dist/lib/cache-manager.d.ts.map +1 -1
- package/dist/lib/cache-manager.js +154 -41
- package/dist/lib/cache-manager.js.map +1 -1
- package/dist/lib/connection-manager.d.ts +7 -0
- package/dist/lib/connection-manager.d.ts.map +1 -1
- package/dist/lib/connection-manager.js +57 -8
- package/dist/lib/connection-manager.js.map +1 -1
- package/dist/lib/proxy-server.d.ts +12 -0
- package/dist/lib/proxy-server.d.ts.map +1 -1
- package/dist/lib/proxy-server.js +184 -87
- package/dist/lib/proxy-server.js.map +1 -1
- package/dist/lib/qwickbrain-client.d.ts +4 -0
- package/dist/lib/qwickbrain-client.d.ts.map +1 -1
- package/dist/lib/qwickbrain-client.js +152 -13
- package/dist/lib/qwickbrain-client.js.map +1 -1
- package/dist/lib/sse-invalidation-listener.d.ts +31 -0
- package/dist/lib/sse-invalidation-listener.d.ts.map +1 -0
- package/dist/lib/sse-invalidation-listener.js +151 -0
- package/dist/lib/sse-invalidation-listener.js.map +1 -0
- package/dist/lib/tools.d.ts +21 -0
- package/dist/lib/tools.d.ts.map +1 -0
- package/dist/lib/tools.js +513 -0
- package/dist/lib/tools.js.map +1 -0
- package/dist/lib/write-queue-manager.d.ts +88 -0
- package/dist/lib/write-queue-manager.d.ts.map +1 -0
- package/dist/lib/write-queue-manager.js +191 -0
- package/dist/lib/write-queue-manager.js.map +1 -0
- package/dist/types/config.d.ts +7 -42
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +1 -6
- package/dist/types/config.js.map +1 -1
- package/drizzle/0002_lru_cache_migration.sql +94 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -2
- package/scripts/rebuild-sqlite.sh +26 -0
- package/src/db/schema.ts +17 -2
- package/src/lib/__tests__/cache-manager.test.ts +180 -90
- package/src/lib/__tests__/connection-manager.test.ts +2 -2
- package/src/lib/__tests__/proxy-server.test.ts +16 -51
- package/src/lib/__tests__/qwickbrain-client.test.ts +3 -1
- package/src/lib/__tests__/sse-invalidation-listener.test.ts +326 -0
- package/src/lib/__tests__/write-queue-manager.test.ts +383 -0
- package/src/lib/cache-manager.ts +198 -46
- package/src/lib/connection-manager.ts +67 -8
- package/src/lib/proxy-server.ts +231 -90
- package/src/lib/qwickbrain-client.ts +166 -12
- package/src/lib/sse-invalidation-listener.ts +185 -0
- package/src/lib/tools.ts +525 -0
- package/src/lib/write-queue-manager.ts +271 -0
- package/src/types/config.ts +1 -6
- package/.github/workflows/publish.yml +0 -92
|
@@ -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
|
+
}
|
package/src/types/config.ts
CHANGED
|
@@ -12,12 +12,7 @@ export const ConfigSchema = z.object({
|
|
|
12
12
|
}).default({}),
|
|
13
13
|
cache: z.object({
|
|
14
14
|
dir: z.string().optional(),
|
|
15
|
-
|
|
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({
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# NPM Publish Workflow for Public Repos
|
|
2
|
-
# Copy this file to .github/workflows/publish.yml in each public repo
|
|
3
|
-
#
|
|
4
|
-
# Required secrets:
|
|
5
|
-
# NPM_TOKEN - npm automation token with publish permissions
|
|
6
|
-
|
|
7
|
-
name: Publish to npm
|
|
8
|
-
|
|
9
|
-
on:
|
|
10
|
-
push:
|
|
11
|
-
branches:
|
|
12
|
-
- main
|
|
13
|
-
paths:
|
|
14
|
-
- 'package.json'
|
|
15
|
-
- 'src/**'
|
|
16
|
-
|
|
17
|
-
# Allow manual publish
|
|
18
|
-
workflow_dispatch:
|
|
19
|
-
|
|
20
|
-
jobs:
|
|
21
|
-
publish:
|
|
22
|
-
runs-on: ubuntu-latest
|
|
23
|
-
permissions:
|
|
24
|
-
contents: write
|
|
25
|
-
steps:
|
|
26
|
-
- uses: actions/checkout@v4
|
|
27
|
-
|
|
28
|
-
- name: Setup Node.js
|
|
29
|
-
uses: actions/setup-node@v4
|
|
30
|
-
with:
|
|
31
|
-
node-version: '20'
|
|
32
|
-
registry-url: 'https://registry.npmjs.org'
|
|
33
|
-
|
|
34
|
-
- name: Install dependencies
|
|
35
|
-
run: npm install --legacy-peer-deps
|
|
36
|
-
|
|
37
|
-
- name: Build
|
|
38
|
-
run: npm run build
|
|
39
|
-
|
|
40
|
-
- name: Run validation
|
|
41
|
-
run: |
|
|
42
|
-
if grep -q '"validate"' package.json; then
|
|
43
|
-
echo "Running validation..."
|
|
44
|
-
npm run validate
|
|
45
|
-
else
|
|
46
|
-
echo "No validation script found, skipping"
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
- name: Check if version changed
|
|
50
|
-
id: version
|
|
51
|
-
run: |
|
|
52
|
-
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
53
|
-
LOCAL_VERSION=$(node -p "require('./package.json').version")
|
|
54
|
-
|
|
55
|
-
# Get published version (returns empty if not published)
|
|
56
|
-
PUBLISHED_VERSION=$(npm view "$PACKAGE_NAME" version 2>/dev/null || echo "")
|
|
57
|
-
|
|
58
|
-
echo "Package: $PACKAGE_NAME"
|
|
59
|
-
echo "Local version: $LOCAL_VERSION"
|
|
60
|
-
echo "Published version: $PUBLISHED_VERSION"
|
|
61
|
-
|
|
62
|
-
if [ "$LOCAL_VERSION" != "$PUBLISHED_VERSION" ]; then
|
|
63
|
-
echo "should_publish=true" >> $GITHUB_OUTPUT
|
|
64
|
-
echo "version=$LOCAL_VERSION" >> $GITHUB_OUTPUT
|
|
65
|
-
else
|
|
66
|
-
echo "should_publish=false" >> $GITHUB_OUTPUT
|
|
67
|
-
echo "Version $LOCAL_VERSION already published"
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
- name: Publish to npm
|
|
71
|
-
if: steps.version.outputs.should_publish == 'true'
|
|
72
|
-
run: npm publish --access public --ignore-scripts
|
|
73
|
-
env:
|
|
74
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
75
|
-
|
|
76
|
-
- name: Create GitHub Release
|
|
77
|
-
if: steps.version.outputs.should_publish == 'true'
|
|
78
|
-
uses: actions/create-release@v1
|
|
79
|
-
env:
|
|
80
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
81
|
-
with:
|
|
82
|
-
tag_name: v${{ steps.version.outputs.version }}
|
|
83
|
-
release_name: v${{ steps.version.outputs.version }}
|
|
84
|
-
body: |
|
|
85
|
-
Published to npm: https://www.npmjs.com/package/${{ github.repository }}
|
|
86
|
-
|
|
87
|
-
Install:
|
|
88
|
-
```bash
|
|
89
|
-
npm install @qwickapps/${{ github.event.repository.name }}@${{ steps.version.outputs.version }}
|
|
90
|
-
```
|
|
91
|
-
draft: false
|
|
92
|
-
prerelease: false
|