agentdb 1.5.8 → 1.6.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/README.md +11 -11
- package/dist/agentdb.min.js +4 -4
- package/dist/cli/agentdb-cli.d.ts +29 -0
- package/dist/cli/agentdb-cli.d.ts.map +1 -1
- package/dist/cli/agentdb-cli.js +1009 -34
- package/dist/cli/agentdb-cli.js.map +1 -1
- package/dist/controllers/ContextSynthesizer.d.ts +65 -0
- package/dist/controllers/ContextSynthesizer.d.ts.map +1 -0
- package/dist/controllers/ContextSynthesizer.js +208 -0
- package/dist/controllers/ContextSynthesizer.js.map +1 -0
- package/dist/controllers/MMRDiversityRanker.d.ts +50 -0
- package/dist/controllers/MMRDiversityRanker.d.ts.map +1 -0
- package/dist/controllers/MMRDiversityRanker.js +130 -0
- package/dist/controllers/MMRDiversityRanker.js.map +1 -0
- package/dist/controllers/MetadataFilter.d.ts +70 -0
- package/dist/controllers/MetadataFilter.d.ts.map +1 -0
- package/dist/controllers/MetadataFilter.js +243 -0
- package/dist/controllers/MetadataFilter.js.map +1 -0
- package/dist/controllers/QUICClient.d.ts +109 -0
- package/dist/controllers/QUICClient.d.ts.map +1 -0
- package/dist/controllers/QUICClient.js +299 -0
- package/dist/controllers/QUICClient.js.map +1 -0
- package/dist/controllers/QUICServer.d.ts +121 -0
- package/dist/controllers/QUICServer.d.ts.map +1 -0
- package/dist/controllers/QUICServer.js +383 -0
- package/dist/controllers/QUICServer.js.map +1 -0
- package/dist/controllers/SyncCoordinator.d.ts +120 -0
- package/dist/controllers/SyncCoordinator.d.ts.map +1 -0
- package/dist/controllers/SyncCoordinator.js +441 -0
- package/dist/controllers/SyncCoordinator.js.map +1 -0
- package/dist/controllers/WASMVectorSearch.d.ts.map +1 -1
- package/dist/controllers/WASMVectorSearch.js +10 -2
- package/dist/controllers/WASMVectorSearch.js.map +1 -1
- package/dist/controllers/index.d.ts +12 -0
- package/dist/controllers/index.d.ts.map +1 -1
- package/dist/controllers/index.js +6 -0
- package/dist/controllers/index.js.map +1 -1
- package/dist/db-fallback.d.ts.map +1 -1
- package/dist/db-fallback.js +14 -11
- package/dist/db-fallback.js.map +1 -1
- package/dist/examples/quic-sync-example.d.ts +9 -0
- package/dist/examples/quic-sync-example.d.ts.map +1 -0
- package/dist/examples/quic-sync-example.js +169 -0
- package/dist/examples/quic-sync-example.js.map +1 -0
- package/dist/types/quic.d.ts +518 -0
- package/dist/types/quic.d.ts.map +1 -0
- package/dist/types/quic.js +272 -0
- package/dist/types/quic.js.map +1 -0
- package/package.json +9 -3
- package/src/browser-entry.js +41 -6
- package/src/cli/agentdb-cli.ts +1114 -33
- package/src/controllers/ContextSynthesizer.ts +285 -0
- package/src/controllers/MMRDiversityRanker.ts +187 -0
- package/src/controllers/MetadataFilter.ts +280 -0
- package/src/controllers/QUICClient.ts +413 -0
- package/src/controllers/QUICServer.ts +498 -0
- package/src/controllers/SyncCoordinator.ts +597 -0
- package/src/controllers/WASMVectorSearch.ts +11 -2
- package/src/controllers/index.ts +12 -0
- package/src/db-fallback.ts +13 -10
- package/src/examples/quic-sync-example.ts +198 -0
- package/src/types/quic.ts +772 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Metadata Filtering
|
|
3
|
+
*
|
|
4
|
+
* Implements MongoDB-style query operators for filtering
|
|
5
|
+
* episodes and patterns based on metadata fields.
|
|
6
|
+
*
|
|
7
|
+
* Supported operators:
|
|
8
|
+
* - $eq: Equal to
|
|
9
|
+
* - $ne: Not equal to
|
|
10
|
+
* - $gt: Greater than
|
|
11
|
+
* - $gte: Greater than or equal to
|
|
12
|
+
* - $lt: Less than
|
|
13
|
+
* - $lte: Less than or equal to
|
|
14
|
+
* - $in: Value is in array
|
|
15
|
+
* - $nin: Value is not in array
|
|
16
|
+
* - $contains: String/array contains value
|
|
17
|
+
* - $exists: Field exists
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type FilterOperator = '$eq' | '$ne' | '$gt' | '$gte' | '$lt' | '$lte' | '$in' | '$nin' | '$contains' | '$exists';
|
|
21
|
+
|
|
22
|
+
export type FilterValue = string | number | boolean | string[] | number[] | { [op in FilterOperator]?: any };
|
|
23
|
+
|
|
24
|
+
export interface MetadataFilters {
|
|
25
|
+
[field: string]: FilterValue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FilterableItem {
|
|
29
|
+
metadata?: any;
|
|
30
|
+
[key: string]: any;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class MetadataFilter {
|
|
34
|
+
/**
|
|
35
|
+
* Apply filters to a collection of items
|
|
36
|
+
*
|
|
37
|
+
* @param items - Items to filter
|
|
38
|
+
* @param filters - MongoDB-style filter object
|
|
39
|
+
* @returns Filtered items
|
|
40
|
+
*/
|
|
41
|
+
static apply<T extends FilterableItem>(items: T[], filters: MetadataFilters): T[] {
|
|
42
|
+
if (!filters || Object.keys(filters).length === 0) {
|
|
43
|
+
return items;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return items.filter(item => this.matchesFilters(item, filters));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if an item matches all filters
|
|
51
|
+
*/
|
|
52
|
+
private static matchesFilters(item: FilterableItem, filters: MetadataFilters): boolean {
|
|
53
|
+
for (const [field, filter] of Object.entries(filters)) {
|
|
54
|
+
if (!this.matchesFilter(item, field, filter)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if an item matches a single filter
|
|
63
|
+
*/
|
|
64
|
+
private static matchesFilter(item: FilterableItem, field: string, filter: FilterValue): boolean {
|
|
65
|
+
// Get field value (supports nested paths like "metadata.year")
|
|
66
|
+
const value = this.getFieldValue(item, field);
|
|
67
|
+
|
|
68
|
+
// Handle simple equality
|
|
69
|
+
if (typeof filter !== 'object' || Array.isArray(filter)) {
|
|
70
|
+
return value === filter;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle operator-based filters
|
|
74
|
+
const operators = filter as { [op in FilterOperator]?: any };
|
|
75
|
+
|
|
76
|
+
for (const [operator, operand] of Object.entries(operators)) {
|
|
77
|
+
switch (operator as FilterOperator) {
|
|
78
|
+
case '$eq':
|
|
79
|
+
if (value !== operand) return false;
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
case '$ne':
|
|
83
|
+
if (value === operand) return false;
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case '$gt':
|
|
87
|
+
if (!(value > operand)) return false;
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case '$gte':
|
|
91
|
+
if (!(value >= operand)) return false;
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case '$lt':
|
|
95
|
+
if (!(value < operand)) return false;
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case '$lte':
|
|
99
|
+
if (!(value <= operand)) return false;
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case '$in':
|
|
103
|
+
if (!Array.isArray(operand) || !operand.includes(value)) return false;
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case '$nin':
|
|
107
|
+
if (!Array.isArray(operand) || operand.includes(value)) return false;
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case '$contains':
|
|
111
|
+
if (typeof value === 'string') {
|
|
112
|
+
if (!value.includes(operand)) return false;
|
|
113
|
+
} else if (Array.isArray(value)) {
|
|
114
|
+
if (!value.includes(operand)) return false;
|
|
115
|
+
} else {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case '$exists':
|
|
121
|
+
const exists = value !== undefined && value !== null;
|
|
122
|
+
if (exists !== operand) return false;
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
default:
|
|
126
|
+
console.warn(`Unknown operator: ${operator}`);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get field value from item (supports nested paths)
|
|
136
|
+
*/
|
|
137
|
+
private static getFieldValue(item: FilterableItem, field: string): any {
|
|
138
|
+
const parts = field.split('.');
|
|
139
|
+
let value: any = item;
|
|
140
|
+
|
|
141
|
+
for (const part of parts) {
|
|
142
|
+
if (value === null || value === undefined) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Parse metadata JSON if needed
|
|
147
|
+
if (part === 'metadata' && typeof value.metadata === 'string') {
|
|
148
|
+
try {
|
|
149
|
+
value.metadata = JSON.parse(value.metadata);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
value = value[part];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build SQL WHERE clause from filters (for database queries)
|
|
163
|
+
*
|
|
164
|
+
* @param filters - Metadata filters
|
|
165
|
+
* @param tableName - Table name for column references
|
|
166
|
+
* @returns SQL WHERE clause and parameters
|
|
167
|
+
*/
|
|
168
|
+
static toSQL(filters: MetadataFilters, tableName: string = ''): { where: string; params: any[] } {
|
|
169
|
+
const conditions: string[] = [];
|
|
170
|
+
const params: any[] = [];
|
|
171
|
+
const prefix = tableName ? `${tableName}.` : '';
|
|
172
|
+
|
|
173
|
+
for (const [field, filter] of Object.entries(filters)) {
|
|
174
|
+
// For metadata fields, use JSON extraction
|
|
175
|
+
const isMetadata = field.startsWith('metadata.');
|
|
176
|
+
const columnRef = isMetadata
|
|
177
|
+
? `json_extract(${prefix}metadata, '$.${field.slice(9)}')`
|
|
178
|
+
: `${prefix}${field}`;
|
|
179
|
+
|
|
180
|
+
if (typeof filter !== 'object' || Array.isArray(filter)) {
|
|
181
|
+
// Simple equality
|
|
182
|
+
conditions.push(`${columnRef} = ?`);
|
|
183
|
+
params.push(filter);
|
|
184
|
+
} else {
|
|
185
|
+
// Operator-based filters
|
|
186
|
+
const operators = filter as { [op in FilterOperator]?: any };
|
|
187
|
+
|
|
188
|
+
for (const [operator, operand] of Object.entries(operators)) {
|
|
189
|
+
switch (operator as FilterOperator) {
|
|
190
|
+
case '$eq':
|
|
191
|
+
conditions.push(`${columnRef} = ?`);
|
|
192
|
+
params.push(operand);
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case '$ne':
|
|
196
|
+
conditions.push(`${columnRef} != ?`);
|
|
197
|
+
params.push(operand);
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case '$gt':
|
|
201
|
+
conditions.push(`${columnRef} > ?`);
|
|
202
|
+
params.push(operand);
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case '$gte':
|
|
206
|
+
conditions.push(`${columnRef} >= ?`);
|
|
207
|
+
params.push(operand);
|
|
208
|
+
break;
|
|
209
|
+
|
|
210
|
+
case '$lt':
|
|
211
|
+
conditions.push(`${columnRef} < ?`);
|
|
212
|
+
params.push(operand);
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case '$lte':
|
|
216
|
+
conditions.push(`${columnRef} <= ?`);
|
|
217
|
+
params.push(operand);
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case '$in':
|
|
221
|
+
if (Array.isArray(operand)) {
|
|
222
|
+
const placeholders = operand.map(() => '?').join(', ');
|
|
223
|
+
conditions.push(`${columnRef} IN (${placeholders})`);
|
|
224
|
+
params.push(...operand);
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
|
|
228
|
+
case '$nin':
|
|
229
|
+
if (Array.isArray(operand)) {
|
|
230
|
+
const placeholders = operand.map(() => '?').join(', ');
|
|
231
|
+
conditions.push(`${columnRef} NOT IN (${placeholders})`);
|
|
232
|
+
params.push(...operand);
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case '$contains':
|
|
237
|
+
conditions.push(`${columnRef} LIKE ?`);
|
|
238
|
+
params.push(`%${operand}%`);
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case '$exists':
|
|
242
|
+
if (operand) {
|
|
243
|
+
conditions.push(`${columnRef} IS NOT NULL`);
|
|
244
|
+
} else {
|
|
245
|
+
conditions.push(`${columnRef} IS NULL`);
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const where = conditions.length > 0 ? conditions.join(' AND ') : '1=1';
|
|
254
|
+
return { where, params };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Validate filter object
|
|
259
|
+
*/
|
|
260
|
+
static validate(filters: MetadataFilters): { valid: boolean; errors: string[] } {
|
|
261
|
+
const errors: string[] = [];
|
|
262
|
+
|
|
263
|
+
for (const [field, filter] of Object.entries(filters)) {
|
|
264
|
+
if (!field || field.trim() === '') {
|
|
265
|
+
errors.push('Filter field name cannot be empty');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (typeof filter === 'object' && !Array.isArray(filter)) {
|
|
269
|
+
const operators = Object.keys(filter);
|
|
270
|
+
for (const op of operators) {
|
|
271
|
+
if (!op.startsWith('$')) {
|
|
272
|
+
errors.push(`Invalid operator: ${op} (must start with $)`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return { valid: errors.length === 0, errors };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUICClient - QUIC Protocol Client for AgentDB Synchronization
|
|
3
|
+
*
|
|
4
|
+
* Implements a QUIC client for initiating synchronization requests to remote
|
|
5
|
+
* AgentDB instances. Supports connection pooling, retry logic, and reliable sync.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Connect to remote QUIC servers
|
|
9
|
+
* - Send sync requests (episodes, skills, edges)
|
|
10
|
+
* - Handle responses and errors
|
|
11
|
+
* - Automatic retry with exponential backoff
|
|
12
|
+
* - Connection pooling for efficiency
|
|
13
|
+
* - Comprehensive error handling
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
|
|
18
|
+
export interface QUICClientConfig {
|
|
19
|
+
serverHost: string;
|
|
20
|
+
serverPort: number;
|
|
21
|
+
authToken?: string;
|
|
22
|
+
maxRetries?: number;
|
|
23
|
+
retryDelayMs?: number;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
poolSize?: number;
|
|
26
|
+
tlsConfig?: {
|
|
27
|
+
cert?: string;
|
|
28
|
+
key?: string;
|
|
29
|
+
ca?: string;
|
|
30
|
+
rejectUnauthorized?: boolean;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SyncOptions {
|
|
35
|
+
type: 'episodes' | 'skills' | 'edges' | 'full';
|
|
36
|
+
since?: number;
|
|
37
|
+
filters?: Record<string, any>;
|
|
38
|
+
batchSize?: number;
|
|
39
|
+
onProgress?: (progress: SyncProgress) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SyncProgress {
|
|
43
|
+
phase: 'connecting' | 'syncing' | 'processing' | 'completed' | 'error';
|
|
44
|
+
itemsSynced?: number;
|
|
45
|
+
totalItems?: number;
|
|
46
|
+
bytesTransferred?: number;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SyncResult {
|
|
51
|
+
success: boolean;
|
|
52
|
+
data?: any;
|
|
53
|
+
itemsReceived: number;
|
|
54
|
+
bytesTransferred: number;
|
|
55
|
+
durationMs: number;
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface Connection {
|
|
60
|
+
id: string;
|
|
61
|
+
inUse: boolean;
|
|
62
|
+
createdAt: number;
|
|
63
|
+
lastUsedAt: number;
|
|
64
|
+
requestCount: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class QUICClient {
|
|
68
|
+
private config: Required<QUICClientConfig>;
|
|
69
|
+
private connectionPool: Map<string, Connection> = new Map();
|
|
70
|
+
private isConnected: boolean = false;
|
|
71
|
+
private retryCount: number = 0;
|
|
72
|
+
|
|
73
|
+
constructor(config: QUICClientConfig) {
|
|
74
|
+
this.config = {
|
|
75
|
+
serverHost: config.serverHost,
|
|
76
|
+
serverPort: config.serverPort,
|
|
77
|
+
authToken: config.authToken || '',
|
|
78
|
+
maxRetries: config.maxRetries || 3,
|
|
79
|
+
retryDelayMs: config.retryDelayMs || 1000,
|
|
80
|
+
timeoutMs: config.timeoutMs || 30000,
|
|
81
|
+
poolSize: config.poolSize || 5,
|
|
82
|
+
tlsConfig: config.tlsConfig || { rejectUnauthorized: true },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Connect to remote QUIC server
|
|
88
|
+
*/
|
|
89
|
+
async connect(): Promise<void> {
|
|
90
|
+
if (this.isConnected) {
|
|
91
|
+
console.log(chalk.yellow('⚠️ Client already connected'));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
console.log(chalk.blue('🔌 Connecting to QUIC server...'));
|
|
97
|
+
console.log(chalk.gray(` Host: ${this.config.serverHost}`));
|
|
98
|
+
console.log(chalk.gray(` Port: ${this.config.serverPort}`));
|
|
99
|
+
|
|
100
|
+
// Note: Actual QUIC implementation would use a library like @fails-components/webtransport
|
|
101
|
+
// or node-quic. This is a reference implementation showing the interface.
|
|
102
|
+
|
|
103
|
+
// Initialize connection pool
|
|
104
|
+
for (let i = 0; i < this.config.poolSize; i++) {
|
|
105
|
+
const connectionId = `conn-${i}`;
|
|
106
|
+
this.connectionPool.set(connectionId, {
|
|
107
|
+
id: connectionId,
|
|
108
|
+
inUse: false,
|
|
109
|
+
createdAt: Date.now(),
|
|
110
|
+
lastUsedAt: 0,
|
|
111
|
+
requestCount: 0,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.isConnected = true;
|
|
116
|
+
this.retryCount = 0;
|
|
117
|
+
|
|
118
|
+
console.log(chalk.green('✓ Connected to QUIC server'));
|
|
119
|
+
console.log(chalk.gray(` Connection pool size: ${this.config.poolSize}`));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const err = error as Error;
|
|
122
|
+
console.error(chalk.red('✗ Connection failed:'), err.message);
|
|
123
|
+
throw new Error(`Failed to connect to QUIC server: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Disconnect from server
|
|
129
|
+
*/
|
|
130
|
+
async disconnect(): Promise<void> {
|
|
131
|
+
if (!this.isConnected) {
|
|
132
|
+
console.log(chalk.yellow('⚠️ Client not connected'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
console.log(chalk.blue('🔌 Disconnecting from QUIC server...'));
|
|
138
|
+
|
|
139
|
+
// Close all connections in pool
|
|
140
|
+
for (const [connId, conn] of this.connectionPool.entries()) {
|
|
141
|
+
console.log(chalk.gray(` Closing connection: ${connId}`));
|
|
142
|
+
// Close connection logic here
|
|
143
|
+
}
|
|
144
|
+
this.connectionPool.clear();
|
|
145
|
+
|
|
146
|
+
this.isConnected = false;
|
|
147
|
+
console.log(chalk.green('✓ Disconnected from QUIC server'));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const err = error as Error;
|
|
150
|
+
console.error(chalk.red('✗ Disconnect error:'), err.message);
|
|
151
|
+
throw new Error(`Failed to disconnect: ${err.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Send sync request to server
|
|
157
|
+
*/
|
|
158
|
+
async sync(options: SyncOptions): Promise<SyncResult> {
|
|
159
|
+
if (!this.isConnected) {
|
|
160
|
+
await this.connect();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const startTime = Date.now();
|
|
164
|
+
let bytesTransferred = 0;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
// Report progress: connecting
|
|
168
|
+
options.onProgress?.({
|
|
169
|
+
phase: 'connecting',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Get connection from pool
|
|
173
|
+
const connection = await this.acquireConnection();
|
|
174
|
+
|
|
175
|
+
console.log(chalk.blue('📤 Sending sync request...'));
|
|
176
|
+
console.log(chalk.gray(` Type: ${options.type}`));
|
|
177
|
+
console.log(chalk.gray(` Since: ${options.since || 'full sync'}`));
|
|
178
|
+
console.log(chalk.gray(` Connection: ${connection.id}`));
|
|
179
|
+
|
|
180
|
+
// Report progress: syncing
|
|
181
|
+
options.onProgress?.({
|
|
182
|
+
phase: 'syncing',
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Prepare request
|
|
186
|
+
const request = {
|
|
187
|
+
type: options.type,
|
|
188
|
+
since: options.since,
|
|
189
|
+
filters: options.filters,
|
|
190
|
+
batchSize: options.batchSize,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Send request with retry logic
|
|
194
|
+
const response = await this.sendWithRetry(connection, request);
|
|
195
|
+
|
|
196
|
+
if (!response.success) {
|
|
197
|
+
throw new Error(response.error || 'Sync request failed');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
bytesTransferred = JSON.stringify(response.data).length;
|
|
201
|
+
|
|
202
|
+
// Report progress: processing
|
|
203
|
+
options.onProgress?.({
|
|
204
|
+
phase: 'processing',
|
|
205
|
+
itemsSynced: response.count,
|
|
206
|
+
bytesTransferred,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Release connection
|
|
210
|
+
this.releaseConnection(connection);
|
|
211
|
+
|
|
212
|
+
const durationMs = Date.now() - startTime;
|
|
213
|
+
|
|
214
|
+
console.log(chalk.green('✓ Sync completed successfully'));
|
|
215
|
+
console.log(chalk.gray(` Items received: ${response.count}`));
|
|
216
|
+
console.log(chalk.gray(` Bytes transferred: ${bytesTransferred}`));
|
|
217
|
+
console.log(chalk.gray(` Duration: ${durationMs}ms`));
|
|
218
|
+
|
|
219
|
+
// Report progress: completed
|
|
220
|
+
options.onProgress?.({
|
|
221
|
+
phase: 'completed',
|
|
222
|
+
itemsSynced: response.count,
|
|
223
|
+
bytesTransferred,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
data: response.data,
|
|
229
|
+
itemsReceived: response.count || 0,
|
|
230
|
+
bytesTransferred,
|
|
231
|
+
durationMs,
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const err = error as Error;
|
|
235
|
+
const durationMs = Date.now() - startTime;
|
|
236
|
+
|
|
237
|
+
console.error(chalk.red('✗ Sync failed:'), err.message);
|
|
238
|
+
|
|
239
|
+
// Report progress: error
|
|
240
|
+
options.onProgress?.({
|
|
241
|
+
phase: 'error',
|
|
242
|
+
error: err.message,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
success: false,
|
|
247
|
+
itemsReceived: 0,
|
|
248
|
+
bytesTransferred,
|
|
249
|
+
durationMs,
|
|
250
|
+
error: err.message,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Send request with automatic retry
|
|
257
|
+
*/
|
|
258
|
+
private async sendWithRetry(
|
|
259
|
+
connection: Connection,
|
|
260
|
+
request: any,
|
|
261
|
+
attempt: number = 0
|
|
262
|
+
): Promise<any> {
|
|
263
|
+
try {
|
|
264
|
+
// Simulate sending request
|
|
265
|
+
// In real implementation, this would use QUIC protocol
|
|
266
|
+
const response = await this.sendRequest(connection, request);
|
|
267
|
+
|
|
268
|
+
// Reset retry count on success
|
|
269
|
+
this.retryCount = 0;
|
|
270
|
+
|
|
271
|
+
return response;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
const err = error as Error;
|
|
274
|
+
|
|
275
|
+
if (attempt < this.config.maxRetries) {
|
|
276
|
+
const delay = this.config.retryDelayMs * Math.pow(2, attempt);
|
|
277
|
+
console.log(chalk.yellow(`⚠️ Request failed, retrying in ${delay}ms (attempt ${attempt + 1}/${this.config.maxRetries})`));
|
|
278
|
+
console.log(chalk.gray(` Error: ${err.message}`));
|
|
279
|
+
|
|
280
|
+
await this.sleep(delay);
|
|
281
|
+
return this.sendWithRetry(connection, request, attempt + 1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
throw new Error(`Sync failed after ${this.config.maxRetries} retries: ${err.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Send request to server
|
|
290
|
+
*/
|
|
291
|
+
private async sendRequest(connection: Connection, request: any): Promise<any> {
|
|
292
|
+
// Simulate request
|
|
293
|
+
// In real implementation, this would serialize and send via QUIC
|
|
294
|
+
|
|
295
|
+
connection.requestCount++;
|
|
296
|
+
connection.lastUsedAt = Date.now();
|
|
297
|
+
|
|
298
|
+
// Simulate network delay
|
|
299
|
+
await this.sleep(100);
|
|
300
|
+
|
|
301
|
+
// Mock response (in real implementation, this comes from server)
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
data: [],
|
|
305
|
+
count: 0,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Acquire connection from pool
|
|
311
|
+
*/
|
|
312
|
+
private async acquireConnection(): Promise<Connection> {
|
|
313
|
+
const timeout = Date.now() + this.config.timeoutMs;
|
|
314
|
+
|
|
315
|
+
while (Date.now() < timeout) {
|
|
316
|
+
for (const connection of this.connectionPool.values()) {
|
|
317
|
+
if (!connection.inUse) {
|
|
318
|
+
connection.inUse = true;
|
|
319
|
+
return connection;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Wait and retry
|
|
324
|
+
await this.sleep(100);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
throw new Error('Connection pool exhausted (timeout)');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Release connection back to pool
|
|
332
|
+
*/
|
|
333
|
+
private releaseConnection(connection: Connection): void {
|
|
334
|
+
connection.inUse = false;
|
|
335
|
+
connection.lastUsedAt = Date.now();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get client status
|
|
340
|
+
*/
|
|
341
|
+
getStatus(): {
|
|
342
|
+
isConnected: boolean;
|
|
343
|
+
poolSize: number;
|
|
344
|
+
activeConnections: number;
|
|
345
|
+
totalRequests: number;
|
|
346
|
+
config: QUICClientConfig;
|
|
347
|
+
} {
|
|
348
|
+
let activeConnections = 0;
|
|
349
|
+
let totalRequests = 0;
|
|
350
|
+
|
|
351
|
+
for (const connection of this.connectionPool.values()) {
|
|
352
|
+
if (connection.inUse) {
|
|
353
|
+
activeConnections++;
|
|
354
|
+
}
|
|
355
|
+
totalRequests += connection.requestCount;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
isConnected: this.isConnected,
|
|
360
|
+
poolSize: this.connectionPool.size,
|
|
361
|
+
activeConnections,
|
|
362
|
+
totalRequests,
|
|
363
|
+
config: this.config,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Test connection to server
|
|
369
|
+
*/
|
|
370
|
+
async ping(): Promise<{ success: boolean; latencyMs: number; error?: string }> {
|
|
371
|
+
const startTime = Date.now();
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
if (!this.isConnected) {
|
|
375
|
+
await this.connect();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const connection = await this.acquireConnection();
|
|
379
|
+
|
|
380
|
+
// Send ping request
|
|
381
|
+
await this.sendRequest(connection, { type: 'ping' });
|
|
382
|
+
|
|
383
|
+
this.releaseConnection(connection);
|
|
384
|
+
|
|
385
|
+
const latencyMs = Date.now() - startTime;
|
|
386
|
+
|
|
387
|
+
console.log(chalk.green(`✓ Ping successful: ${latencyMs}ms`));
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
success: true,
|
|
391
|
+
latencyMs,
|
|
392
|
+
};
|
|
393
|
+
} catch (error) {
|
|
394
|
+
const err = error as Error;
|
|
395
|
+
const latencyMs = Date.now() - startTime;
|
|
396
|
+
|
|
397
|
+
console.error(chalk.red('✗ Ping failed:'), err.message);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
success: false,
|
|
401
|
+
latencyMs,
|
|
402
|
+
error: err.message,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Sleep helper
|
|
409
|
+
*/
|
|
410
|
+
private sleep(ms: number): Promise<void> {
|
|
411
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
412
|
+
}
|
|
413
|
+
}
|