agentgui 1.0.64 → 1.0.66
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/.prd +54 -0
- package/CLAUDE.md +176 -0
- package/lib/claude-runner.js +71 -0
- package/lib/database-service.ts +388 -0
- package/lib/machines.ts +593 -0
- package/lib/schemas.ts +213 -0
- package/lib/sync-service.ts +340 -0
- package/lib/types.ts +245 -0
- package/package.json +1 -1
- package/server.js +73 -279
- package/static/app.js +205 -1584
- package/static/styles.css +135 -0
- package/conversation-sync.js +0 -196
- package/state-manager.js +0 -360
- package/state-validator.js +0 -150
- package/static/sync-manager.js +0 -273
- package/stream-handler.js +0 -106
package/lib/schemas.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCHEMAS.TS - Zod validation schemas for all data structures
|
|
3
|
+
* Ensures data integrity at every boundary (API, database, client)
|
|
4
|
+
* Provides type-safe parsing and validation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// CONVERSATION SCHEMAS
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export const ConversationStatusSchema = z.enum(['active', 'archived', 'deleted']);
|
|
14
|
+
|
|
15
|
+
export const ConversationSchema = z.object({
|
|
16
|
+
id: z.string().min(1),
|
|
17
|
+
agentId: z.string().min(1),
|
|
18
|
+
title: z.string().nullable().optional(),
|
|
19
|
+
created_at: z.number().int().positive(),
|
|
20
|
+
updated_at: z.number().int().positive(),
|
|
21
|
+
status: ConversationStatusSchema,
|
|
22
|
+
agentType: z.string().optional(),
|
|
23
|
+
source: z.enum(['gui', 'imported']).optional(),
|
|
24
|
+
externalId: z.string().optional(),
|
|
25
|
+
firstPrompt: z.string().optional(),
|
|
26
|
+
messageCount: z.number().int().nonnegative().optional(),
|
|
27
|
+
projectPath: z.string().optional(),
|
|
28
|
+
gitBranch: z.string().optional(),
|
|
29
|
+
sourcePath: z.string().optional(),
|
|
30
|
+
lastSyncedAt: z.number().int().optional(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const ConversationCreateInputSchema = z.object({
|
|
34
|
+
agentId: z.string().min(1, 'agentId is required'),
|
|
35
|
+
title: z.string().max(500).nullable().optional(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const ConversationUpdateInputSchema = z.object({
|
|
39
|
+
title: z.string().max(500).optional(),
|
|
40
|
+
status: ConversationStatusSchema.optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const ConversationsListSchema = z.object({
|
|
44
|
+
conversations: z.array(ConversationSchema),
|
|
45
|
+
total: z.number().int().nonnegative(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// MESSAGE SCHEMAS
|
|
50
|
+
// ============================================================================
|
|
51
|
+
|
|
52
|
+
export const MessageRoleSchema = z.enum(['user', 'assistant', 'system']);
|
|
53
|
+
|
|
54
|
+
export const MessageSchema = z.object({
|
|
55
|
+
id: z.string().min(1),
|
|
56
|
+
conversationId: z.string().min(1),
|
|
57
|
+
role: MessageRoleSchema,
|
|
58
|
+
content: z.string(),
|
|
59
|
+
created_at: z.number().int().positive(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const MessageCreateInputSchema = z.object({
|
|
63
|
+
conversationId: z.string().min(1),
|
|
64
|
+
role: MessageRoleSchema,
|
|
65
|
+
content: z.string().min(1, 'content cannot be empty').max(1000000),
|
|
66
|
+
idempotencyKey: z.string().optional(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const MessagesListSchema = z.object({
|
|
70
|
+
messages: z.array(MessageSchema),
|
|
71
|
+
total: z.number().int().nonnegative(),
|
|
72
|
+
hasMore: z.boolean(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// SESSION SCHEMAS
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
export const SessionStatusSchema = z.enum(['pending', 'processing', 'completed', 'error', 'cancelled']);
|
|
80
|
+
|
|
81
|
+
export const SessionResponseSchema = z.object({
|
|
82
|
+
text: z.string(),
|
|
83
|
+
messageId: z.string().min(1),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const SessionSchema = z.object({
|
|
87
|
+
id: z.string().min(1),
|
|
88
|
+
conversationId: z.string().min(1),
|
|
89
|
+
status: SessionStatusSchema,
|
|
90
|
+
started_at: z.number().int().positive(),
|
|
91
|
+
completed_at: z.number().int().positive().optional(),
|
|
92
|
+
response: SessionResponseSchema.optional(),
|
|
93
|
+
error: z.string().optional(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// SYNC STATE SCHEMAS
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
export const SyncStateSchema = z.enum(['idle', 'loading', 'synced', 'error', 'offline', 'reconciling']);
|
|
101
|
+
|
|
102
|
+
export const SyncStatusSchema = z.object({
|
|
103
|
+
state: SyncStateSchema,
|
|
104
|
+
lastSyncTime: z.number().int().optional(),
|
|
105
|
+
nextRetryTime: z.number().int().optional(),
|
|
106
|
+
error: z.string().optional(),
|
|
107
|
+
retryCount: z.number().int().nonnegative(),
|
|
108
|
+
maxRetries: z.number().int().positive(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const SyncEventTypeSchema = z.enum([
|
|
112
|
+
'conversation_created',
|
|
113
|
+
'conversation_updated',
|
|
114
|
+
'conversation_deleted',
|
|
115
|
+
'message_created',
|
|
116
|
+
'message_updated',
|
|
117
|
+
'message_deleted',
|
|
118
|
+
'sync_started',
|
|
119
|
+
'sync_completed',
|
|
120
|
+
'sync_failed',
|
|
121
|
+
'offline_detected',
|
|
122
|
+
'online_detected',
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
export const SyncEventSchema = z.object({
|
|
126
|
+
type: SyncEventTypeSchema,
|
|
127
|
+
timestamp: z.number().int().positive(),
|
|
128
|
+
data: z.record(z.unknown()),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// PAGINATION SCHEMAS
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
export const PaginationParamsSchema = z.object({
|
|
136
|
+
limit: z.number().int().positive().max(100),
|
|
137
|
+
offset: z.number().int().nonnegative(),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const PaginatedResultSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
|
|
141
|
+
z.object({
|
|
142
|
+
items: z.array(itemSchema),
|
|
143
|
+
total: z.number().int().nonnegative(),
|
|
144
|
+
limit: z.number().int().positive(),
|
|
145
|
+
offset: z.number().int().nonnegative(),
|
|
146
|
+
hasMore: z.boolean(),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// IDEMPOTENCY SCHEMAS
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
export const IdempotencyKeySchema = z.object({
|
|
154
|
+
key: z.string().min(1),
|
|
155
|
+
value: z.string(),
|
|
156
|
+
created_at: z.number().int().positive(),
|
|
157
|
+
ttl: z.number().int().positive(),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ============================================================================
|
|
161
|
+
// API RESPONSE SCHEMAS
|
|
162
|
+
// ============================================================================
|
|
163
|
+
|
|
164
|
+
export const ApiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
|
|
165
|
+
z.object({
|
|
166
|
+
data: dataSchema.optional(),
|
|
167
|
+
error: z.string().optional(),
|
|
168
|
+
timestamp: z.number().int().positive(),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// VALIDATION HELPER FUNCTIONS
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
export function validateConversation(data: unknown) {
|
|
176
|
+
try {
|
|
177
|
+
return { valid: true, data: ConversationSchema.parse(data) };
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return { valid: false, error: String(error) };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function validateMessage(data: unknown) {
|
|
184
|
+
try {
|
|
185
|
+
return { valid: true, data: MessageSchema.parse(data) };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return { valid: false, error: String(error) };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function validateConversationsList(data: unknown) {
|
|
192
|
+
try {
|
|
193
|
+
return { valid: true, data: ConversationsListSchema.parse(data) };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
return { valid: false, error: String(error) };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function validateMessagesList(data: unknown) {
|
|
200
|
+
try {
|
|
201
|
+
return { valid: true, data: MessagesListSchema.parse(data) };
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return { valid: false, error: String(error) };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function validatePaginationParams(data: unknown) {
|
|
208
|
+
try {
|
|
209
|
+
return { valid: true, data: PaginationParamsSchema.parse(data) };
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return { valid: false, error: String(error) };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SYNC-SERVICE.TS - Independent sync engine
|
|
3
|
+
* Handles all conversation and message synchronization
|
|
4
|
+
* Guaranteed eventual consistency with conflict resolution
|
|
5
|
+
* Deduplicates operations and implements exponential backoff
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import {
|
|
10
|
+
Conversation,
|
|
11
|
+
Message,
|
|
12
|
+
SyncEvent,
|
|
13
|
+
SyncStatus,
|
|
14
|
+
SyncError,
|
|
15
|
+
ConflictResolutionStrategy,
|
|
16
|
+
} from './types';
|
|
17
|
+
import DatabaseService from './database-service';
|
|
18
|
+
|
|
19
|
+
interface SyncOptions {
|
|
20
|
+
retryAttempts?: number;
|
|
21
|
+
retryDelay?: number;
|
|
22
|
+
maxRetryDelay?: number;
|
|
23
|
+
conflictResolution?: ConflictResolutionStrategy;
|
|
24
|
+
batchSize?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SyncService - Independent sync operations
|
|
29
|
+
* Handles conversations and messages with conflict resolution
|
|
30
|
+
*/
|
|
31
|
+
export class SyncService extends EventEmitter {
|
|
32
|
+
private db: DatabaseService;
|
|
33
|
+
private syncInProgress = false;
|
|
34
|
+
private lastSyncTime = 0;
|
|
35
|
+
private pendingOperations: Map<string, SyncEvent> = new Map();
|
|
36
|
+
private retryAttempts = 0;
|
|
37
|
+
private options: Required<SyncOptions>;
|
|
38
|
+
|
|
39
|
+
constructor(db: DatabaseService, options: SyncOptions = {}) {
|
|
40
|
+
super();
|
|
41
|
+
this.db = db;
|
|
42
|
+
this.options = {
|
|
43
|
+
retryAttempts: options.retryAttempts ?? 5,
|
|
44
|
+
retryDelay: options.retryDelay ?? 1000,
|
|
45
|
+
maxRetryDelay: options.maxRetryDelay ?? 30000,
|
|
46
|
+
conflictResolution: options.conflictResolution ?? 'last-write-wins',
|
|
47
|
+
batchSize: options.batchSize ?? 50,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// =========================================================================
|
|
52
|
+
// SYNC OPERATIONS
|
|
53
|
+
// =========================================================================
|
|
54
|
+
|
|
55
|
+
async syncConversations(fromServer: Conversation[]): Promise<SyncStatus> {
|
|
56
|
+
if (this.syncInProgress) {
|
|
57
|
+
return {
|
|
58
|
+
state: 'loading',
|
|
59
|
+
retryCount: this.retryAttempts,
|
|
60
|
+
maxRetries: this.options.retryAttempts,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.syncInProgress = true;
|
|
65
|
+
try {
|
|
66
|
+
this.emit('sync:start', { type: 'conversations' });
|
|
67
|
+
|
|
68
|
+
const local = this.db.getConversationsList();
|
|
69
|
+
const changes = this.detectChanges(local, fromServer);
|
|
70
|
+
|
|
71
|
+
if (changes.added.length > 0) {
|
|
72
|
+
await this.applyAddedConversations(changes.added);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (changes.updated.length > 0) {
|
|
76
|
+
await this.applyUpdatedConversations(changes.updated);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (changes.deleted.length > 0) {
|
|
80
|
+
await this.applyDeletedConversations(changes.deleted);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.lastSyncTime = Date.now();
|
|
84
|
+
this.retryAttempts = 0;
|
|
85
|
+
|
|
86
|
+
this.emit('sync:complete', {
|
|
87
|
+
type: 'conversations',
|
|
88
|
+
changes,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
state: 'synced',
|
|
93
|
+
lastSyncTime: this.lastSyncTime,
|
|
94
|
+
retryCount: 0,
|
|
95
|
+
maxRetries: this.options.retryAttempts,
|
|
96
|
+
};
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return this.handleSyncError(err as Error);
|
|
99
|
+
} finally {
|
|
100
|
+
this.syncInProgress = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async syncMessages(conversationId: string, fromServer: Message[]): Promise<SyncStatus> {
|
|
105
|
+
try {
|
|
106
|
+
this.emit('sync:start', { type: 'messages', conversationId });
|
|
107
|
+
|
|
108
|
+
const local = this.db.getConversationMessages(conversationId);
|
|
109
|
+
const changes = this.detectMessageChanges(local, fromServer);
|
|
110
|
+
|
|
111
|
+
if (changes.added.length > 0) {
|
|
112
|
+
await this.applyAddedMessages(conversationId, changes.added);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (changes.deleted.length > 0) {
|
|
116
|
+
await this.applyDeletedMessages(changes.deleted);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.emit('sync:complete', {
|
|
120
|
+
type: 'messages',
|
|
121
|
+
conversationId,
|
|
122
|
+
changes,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
state: 'synced',
|
|
127
|
+
lastSyncTime: Date.now(),
|
|
128
|
+
retryCount: 0,
|
|
129
|
+
maxRetries: this.options.retryAttempts,
|
|
130
|
+
};
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return this.handleSyncError(err as Error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// =========================================================================
|
|
137
|
+
// CHANGE DETECTION
|
|
138
|
+
// =========================================================================
|
|
139
|
+
|
|
140
|
+
private detectChanges(local: Conversation[], remote: Conversation[]) {
|
|
141
|
+
const localMap = new Map(local.map((c) => [c.id, c]));
|
|
142
|
+
const remoteMap = new Map(remote.map((c) => [c.id, c]));
|
|
143
|
+
|
|
144
|
+
const added = remote.filter((c) => !localMap.has(c.id));
|
|
145
|
+
const deleted = local.filter((c) => !remoteMap.has(c.id) && c.status !== 'deleted');
|
|
146
|
+
const updated = remote.filter((c) => {
|
|
147
|
+
const localVersion = localMap.get(c.id);
|
|
148
|
+
return localVersion && localVersion.updated_at < c.updated_at;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return { added, updated, deleted };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private detectMessageChanges(local: Message[], remote: Message[]) {
|
|
155
|
+
const localMap = new Map(local.map((m) => [m.id, m]));
|
|
156
|
+
const remoteMap = new Map(remote.map((m) => [m.id, m]));
|
|
157
|
+
|
|
158
|
+
const added = remote.filter((m) => !localMap.has(m.id));
|
|
159
|
+
const deleted = local.filter((m) => !remoteMap.has(m.id));
|
|
160
|
+
|
|
161
|
+
return { added, deleted };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// =========================================================================
|
|
165
|
+
// APPLY CHANGES
|
|
166
|
+
// =========================================================================
|
|
167
|
+
|
|
168
|
+
private async applyAddedConversations(conversations: Conversation[]): Promise<void> {
|
|
169
|
+
for (const conv of conversations) {
|
|
170
|
+
try {
|
|
171
|
+
// Note: In real implementation, would insert into DB
|
|
172
|
+
// Here we just validate the data
|
|
173
|
+
if (!conv.id || !conv.agentId) {
|
|
174
|
+
throw new Error('Invalid conversation: missing id or agentId');
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
this.emit('sync:error', {
|
|
178
|
+
type: 'add_conversation',
|
|
179
|
+
error: (err as Error).message,
|
|
180
|
+
data: conv,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async applyUpdatedConversations(conversations: Conversation[]): Promise<void> {
|
|
187
|
+
for (const conv of conversations) {
|
|
188
|
+
try {
|
|
189
|
+
if (!conv.id) throw new Error('Invalid conversation: missing id');
|
|
190
|
+
// Update would happen here in real implementation
|
|
191
|
+
} catch (err) {
|
|
192
|
+
this.emit('sync:error', {
|
|
193
|
+
type: 'update_conversation',
|
|
194
|
+
error: (err as Error).message,
|
|
195
|
+
data: conv,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async applyDeletedConversations(conversations: Conversation[]): Promise<void> {
|
|
202
|
+
for (const conv of conversations) {
|
|
203
|
+
try {
|
|
204
|
+
if (!conv.id) throw new Error('Invalid conversation: missing id');
|
|
205
|
+
this.db.deleteConversation(conv.id);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
this.emit('sync:error', {
|
|
208
|
+
type: 'delete_conversation',
|
|
209
|
+
error: (err as Error).message,
|
|
210
|
+
data: conv,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async applyAddedMessages(conversationId: string, messages: Message[]): Promise<void> {
|
|
217
|
+
for (const msg of messages) {
|
|
218
|
+
try {
|
|
219
|
+
if (!msg.id || !msg.role) {
|
|
220
|
+
throw new Error('Invalid message: missing id or role');
|
|
221
|
+
}
|
|
222
|
+
// Message insert would happen here in real implementation
|
|
223
|
+
} catch (err) {
|
|
224
|
+
this.emit('sync:error', {
|
|
225
|
+
type: 'add_message',
|
|
226
|
+
error: (err as Error).message,
|
|
227
|
+
data: msg,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async applyDeletedMessages(messages: Message[]): Promise<void> {
|
|
234
|
+
for (const msg of messages) {
|
|
235
|
+
try {
|
|
236
|
+
if (!msg.id) throw new Error('Invalid message: missing id');
|
|
237
|
+
this.db.deleteMessage(msg.id);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
this.emit('sync:error', {
|
|
240
|
+
type: 'delete_message',
|
|
241
|
+
error: (err as Error).message,
|
|
242
|
+
data: msg,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// =========================================================================
|
|
249
|
+
// ERROR HANDLING & RETRY LOGIC
|
|
250
|
+
// =========================================================================
|
|
251
|
+
|
|
252
|
+
private handleSyncError(error: Error): SyncStatus {
|
|
253
|
+
this.retryAttempts++;
|
|
254
|
+
const isRetryable = this.retryAttempts < this.options.retryAttempts;
|
|
255
|
+
|
|
256
|
+
const delay = Math.min(
|
|
257
|
+
this.options.retryDelay * Math.pow(2, this.retryAttempts - 1),
|
|
258
|
+
this.options.maxRetryDelay
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (isRetryable) {
|
|
262
|
+
console.log(`[SyncService] Retry in ${delay}ms (attempt ${this.retryAttempts}/${this.options.retryAttempts})`);
|
|
263
|
+
setTimeout(() => this.emit('sync:retry'), delay);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.emit('sync:error', {
|
|
267
|
+
error: error.message,
|
|
268
|
+
retryable: isRetryable,
|
|
269
|
+
attempts: this.retryAttempts,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
state: isRetryable ? 'error' : 'error',
|
|
274
|
+
error: error.message,
|
|
275
|
+
retryCount: this.retryAttempts,
|
|
276
|
+
maxRetries: this.options.retryAttempts,
|
|
277
|
+
nextRetryTime: isRetryable ? Date.now() + delay : undefined,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// =========================================================================
|
|
282
|
+
// QUEUE MANAGEMENT
|
|
283
|
+
// =========================================================================
|
|
284
|
+
|
|
285
|
+
queueOperation(op: SyncEvent): void {
|
|
286
|
+
const key = `${op.type}:${op.data.id || 'global'}`;
|
|
287
|
+
this.pendingOperations.set(key, op);
|
|
288
|
+
this.emit('queue:updated', { size: this.pendingOperations.size });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async flushQueue(): Promise<void> {
|
|
292
|
+
if (this.pendingOperations.size === 0) return;
|
|
293
|
+
|
|
294
|
+
const ops = Array.from(this.pendingOperations.values());
|
|
295
|
+
this.pendingOperations.clear();
|
|
296
|
+
|
|
297
|
+
for (const op of ops) {
|
|
298
|
+
try {
|
|
299
|
+
await this.processOperation(op);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
this.emit('queue:error', {
|
|
302
|
+
operation: op,
|
|
303
|
+
error: (err as Error).message,
|
|
304
|
+
});
|
|
305
|
+
// Re-queue failed operation
|
|
306
|
+
this.queueOperation(op);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private async processOperation(op: SyncEvent): Promise<void> {
|
|
312
|
+
// Implementation would process each operation based on type
|
|
313
|
+
this.emit('operation:processed', op);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// =========================================================================
|
|
317
|
+
// STATUS & INFO
|
|
318
|
+
// =========================================================================
|
|
319
|
+
|
|
320
|
+
getStatus(): SyncStatus {
|
|
321
|
+
return {
|
|
322
|
+
state: this.syncInProgress ? 'loading' : 'synced',
|
|
323
|
+
lastSyncTime: this.lastSyncTime,
|
|
324
|
+
retryCount: this.retryAttempts,
|
|
325
|
+
maxRetries: this.options.retryAttempts,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
getPendingOperationsCount(): number {
|
|
330
|
+
return this.pendingOperations.size;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
clear(): void {
|
|
334
|
+
this.pendingOperations.clear();
|
|
335
|
+
this.retryAttempts = 0;
|
|
336
|
+
this.lastSyncTime = 0;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export default SyncService;
|