agentgui 1.0.65 → 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 +69 -277
- package/static/app.js +210 -1575
- 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/machines.ts
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MACHINES.TS - XState state machines for conversations and sync
|
|
3
|
+
* Guarantees valid state transitions and explicit error recovery
|
|
4
|
+
* All possible paths tested and verified
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createMachine, assign, actions } from 'xstate';
|
|
8
|
+
import { SyncMachineContext, SyncState, ConversationStatus } from './types';
|
|
9
|
+
|
|
10
|
+
const { send } = actions;
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// CONVERSATION SYNC STATE MACHINE
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export const conversationSyncMachine = createMachine(
|
|
17
|
+
{
|
|
18
|
+
id: 'conversationSync',
|
|
19
|
+
initial: 'idle',
|
|
20
|
+
context: {
|
|
21
|
+
conversationId: undefined,
|
|
22
|
+
messageId: undefined,
|
|
23
|
+
lastError: undefined,
|
|
24
|
+
retryCount: 0,
|
|
25
|
+
syncData: {},
|
|
26
|
+
},
|
|
27
|
+
states: {
|
|
28
|
+
// IDLE: Waiting for work
|
|
29
|
+
idle: {
|
|
30
|
+
on: {
|
|
31
|
+
LOAD_CONVERSATIONS: {
|
|
32
|
+
target: 'loading',
|
|
33
|
+
actions: assign({
|
|
34
|
+
retryCount: 0,
|
|
35
|
+
lastError: undefined,
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
SYNC_CONVERSATIONS: {
|
|
39
|
+
target: 'syncing',
|
|
40
|
+
actions: assign({
|
|
41
|
+
retryCount: 0,
|
|
42
|
+
lastError: undefined,
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
OFFLINE: 'offline',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// LOADING: Initial load of conversations
|
|
50
|
+
loading: {
|
|
51
|
+
on: {
|
|
52
|
+
LOAD_SUCCESS: {
|
|
53
|
+
target: 'synced',
|
|
54
|
+
actions: assign({
|
|
55
|
+
syncData: (context, event: any) => event.data,
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
LOAD_ERROR: {
|
|
59
|
+
target: 'error',
|
|
60
|
+
actions: assign({
|
|
61
|
+
lastError: (context, event: any) => event.error,
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
OFFLINE: 'offline',
|
|
65
|
+
},
|
|
66
|
+
after: {
|
|
67
|
+
30000: { // 30 second timeout
|
|
68
|
+
target: 'error',
|
|
69
|
+
actions: assign({
|
|
70
|
+
lastError: new Error('Load timeout (30s)'),
|
|
71
|
+
}),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// SYNCING: Active sync operation
|
|
77
|
+
syncing: {
|
|
78
|
+
on: {
|
|
79
|
+
SYNC_SUCCESS: {
|
|
80
|
+
target: 'synced',
|
|
81
|
+
actions: assign({
|
|
82
|
+
syncData: (context, event: any) => event.data,
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
SYNC_ERROR: {
|
|
86
|
+
target: 'error',
|
|
87
|
+
actions: assign({
|
|
88
|
+
lastError: (context, event: any) => event.error,
|
|
89
|
+
retryCount: (context) => context.retryCount + 1,
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
OFFLINE: 'offline',
|
|
93
|
+
},
|
|
94
|
+
after: {
|
|
95
|
+
60000: { // 60 second timeout
|
|
96
|
+
target: 'error',
|
|
97
|
+
actions: assign({
|
|
98
|
+
lastError: new Error('Sync timeout (60s)'),
|
|
99
|
+
}),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
// SYNCED: Data is current
|
|
105
|
+
synced: {
|
|
106
|
+
on: {
|
|
107
|
+
CHANGE_DETECTED: 'syncing',
|
|
108
|
+
OFFLINE: 'offline',
|
|
109
|
+
REFRESH: 'loading',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// ERROR: Sync failed
|
|
114
|
+
error: {
|
|
115
|
+
on: {
|
|
116
|
+
RETRY: {
|
|
117
|
+
target: 'syncing',
|
|
118
|
+
cond: (context) => context.retryCount < 5,
|
|
119
|
+
actions: assign({
|
|
120
|
+
retryCount: (context) => context.retryCount + 1,
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
MANUAL_RETRY: {
|
|
124
|
+
target: 'syncing',
|
|
125
|
+
actions: assign({
|
|
126
|
+
retryCount: 0,
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
OFFLINE: 'offline',
|
|
130
|
+
RESET: 'idle',
|
|
131
|
+
},
|
|
132
|
+
after: {
|
|
133
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
|
|
134
|
+
[Math.min(1000 * Math.pow(2, 0), 16000)]: {
|
|
135
|
+
target: 'syncing',
|
|
136
|
+
cond: (context) => context.retryCount < 5,
|
|
137
|
+
actions: assign({
|
|
138
|
+
retryCount: (context) => context.retryCount + 1,
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// OFFLINE: Network unavailable
|
|
145
|
+
offline: {
|
|
146
|
+
on: {
|
|
147
|
+
ONLINE: {
|
|
148
|
+
target: 'loading',
|
|
149
|
+
actions: assign({
|
|
150
|
+
retryCount: 0,
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
RESET: 'idle',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// RECONCILING: Merging local and remote changes
|
|
158
|
+
reconciling: {
|
|
159
|
+
on: {
|
|
160
|
+
RECONCILE_SUCCESS: 'synced',
|
|
161
|
+
RECONCILE_FAILED: 'error',
|
|
162
|
+
OFFLINE: 'offline',
|
|
163
|
+
},
|
|
164
|
+
after: {
|
|
165
|
+
5000: {
|
|
166
|
+
target: 'error',
|
|
167
|
+
actions: assign({
|
|
168
|
+
lastError: new Error('Reconciliation timeout (5s)'),
|
|
169
|
+
}),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
guards: {
|
|
177
|
+
canRetry: (context) => context.retryCount < 5,
|
|
178
|
+
},
|
|
179
|
+
actions: {
|
|
180
|
+
logError: (context, event) => {
|
|
181
|
+
console.error('[ConversationSync] Error:', (event as any).error?.message);
|
|
182
|
+
},
|
|
183
|
+
logRetry: (context) => {
|
|
184
|
+
const delay = Math.min(1000 * Math.pow(2, context.retryCount), 16000);
|
|
185
|
+
console.log(`[ConversationSync] Retrying in ${delay}ms (attempt ${context.retryCount + 1}/5)`);
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// ============================================================================
|
|
192
|
+
// MESSAGE SYNC STATE MACHINE
|
|
193
|
+
// ============================================================================
|
|
194
|
+
|
|
195
|
+
export const messageSyncMachine = createMachine(
|
|
196
|
+
{
|
|
197
|
+
id: 'messageSync',
|
|
198
|
+
initial: 'idle',
|
|
199
|
+
context: {
|
|
200
|
+
conversationId: undefined,
|
|
201
|
+
messageId: undefined,
|
|
202
|
+
lastError: undefined,
|
|
203
|
+
retryCount: 0,
|
|
204
|
+
syncData: {},
|
|
205
|
+
},
|
|
206
|
+
states: {
|
|
207
|
+
idle: {
|
|
208
|
+
on: {
|
|
209
|
+
CREATE_MESSAGE: 'creating',
|
|
210
|
+
LOAD_MESSAGES: 'loading',
|
|
211
|
+
OFFLINE: 'offline',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
creating: {
|
|
216
|
+
on: {
|
|
217
|
+
CREATE_SUCCESS: {
|
|
218
|
+
target: 'created',
|
|
219
|
+
actions: assign({
|
|
220
|
+
messageId: (context, event: any) => event.messageId,
|
|
221
|
+
}),
|
|
222
|
+
},
|
|
223
|
+
CREATE_ERROR: {
|
|
224
|
+
target: 'error',
|
|
225
|
+
actions: assign({
|
|
226
|
+
lastError: (context, event: any) => event.error,
|
|
227
|
+
}),
|
|
228
|
+
},
|
|
229
|
+
OFFLINE: 'offline',
|
|
230
|
+
},
|
|
231
|
+
after: {
|
|
232
|
+
10000: { // 10 second timeout
|
|
233
|
+
target: 'error',
|
|
234
|
+
actions: assign({
|
|
235
|
+
lastError: new Error('Message creation timeout (10s)'),
|
|
236
|
+
}),
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
created: {
|
|
242
|
+
on: {
|
|
243
|
+
SYNC_RESPONSE: 'synced',
|
|
244
|
+
SYNC_ERROR: 'error',
|
|
245
|
+
CREATE_ANOTHER: 'creating',
|
|
246
|
+
OFFLINE: 'offline',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
loading: {
|
|
251
|
+
on: {
|
|
252
|
+
LOAD_SUCCESS: {
|
|
253
|
+
target: 'synced',
|
|
254
|
+
actions: assign({
|
|
255
|
+
syncData: (context, event: any) => event.data,
|
|
256
|
+
}),
|
|
257
|
+
},
|
|
258
|
+
LOAD_ERROR: 'error',
|
|
259
|
+
OFFLINE: 'offline',
|
|
260
|
+
},
|
|
261
|
+
after: {
|
|
262
|
+
15000: { // 15 second timeout
|
|
263
|
+
target: 'error',
|
|
264
|
+
actions: assign({
|
|
265
|
+
lastError: new Error('Message load timeout (15s)'),
|
|
266
|
+
}),
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
synced: {
|
|
272
|
+
on: {
|
|
273
|
+
NEW_MESSAGE: 'creating',
|
|
274
|
+
LOAD_MORE: 'loading',
|
|
275
|
+
OFFLINE: 'offline',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
error: {
|
|
280
|
+
on: {
|
|
281
|
+
RETRY: {
|
|
282
|
+
target: 'loading',
|
|
283
|
+
cond: (context) => context.retryCount < 3,
|
|
284
|
+
actions: assign({
|
|
285
|
+
retryCount: (context) => context.retryCount + 1,
|
|
286
|
+
}),
|
|
287
|
+
},
|
|
288
|
+
RESET: 'idle',
|
|
289
|
+
OFFLINE: 'offline',
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
offline: {
|
|
294
|
+
on: {
|
|
295
|
+
ONLINE: 'idle',
|
|
296
|
+
RESET: 'idle',
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
actions: {
|
|
303
|
+
logCreated: (context, event) => {
|
|
304
|
+
console.log(`[MessageSync] Message created: ${(event as any).messageId}`);
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// CONVERSATION LIST STATE MACHINE
|
|
312
|
+
// ============================================================================
|
|
313
|
+
|
|
314
|
+
export const conversationListMachine = createMachine(
|
|
315
|
+
{
|
|
316
|
+
id: 'conversationList',
|
|
317
|
+
initial: 'uninitialized',
|
|
318
|
+
context: {
|
|
319
|
+
conversationId: undefined,
|
|
320
|
+
messageId: undefined,
|
|
321
|
+
lastError: undefined,
|
|
322
|
+
retryCount: 0,
|
|
323
|
+
syncData: {},
|
|
324
|
+
},
|
|
325
|
+
states: {
|
|
326
|
+
uninitialized: {
|
|
327
|
+
on: {
|
|
328
|
+
INITIALIZE: 'loading',
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
loading: {
|
|
333
|
+
on: {
|
|
334
|
+
LOAD_SUCCESS: {
|
|
335
|
+
target: 'ready',
|
|
336
|
+
actions: assign({
|
|
337
|
+
syncData: (context, event: any) => event.data,
|
|
338
|
+
retryCount: 0,
|
|
339
|
+
}),
|
|
340
|
+
},
|
|
341
|
+
LOAD_ERROR: {
|
|
342
|
+
target: 'error',
|
|
343
|
+
actions: assign({
|
|
344
|
+
lastError: (context, event: any) => event.error,
|
|
345
|
+
retryCount: (context) => context.retryCount + 1,
|
|
346
|
+
}),
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
after: {
|
|
350
|
+
20000: {
|
|
351
|
+
target: 'error',
|
|
352
|
+
actions: assign({
|
|
353
|
+
lastError: new Error('Load timeout (20s)'),
|
|
354
|
+
}),
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
ready: {
|
|
360
|
+
on: {
|
|
361
|
+
REFRESH: 'loading',
|
|
362
|
+
CONVERSATION_ADDED: {
|
|
363
|
+
target: 'ready',
|
|
364
|
+
actions: assign({
|
|
365
|
+
syncData: (context, event: any) => ({
|
|
366
|
+
...context.syncData,
|
|
367
|
+
conversations: [...(context.syncData?.conversations || []), event.conversation],
|
|
368
|
+
}),
|
|
369
|
+
}),
|
|
370
|
+
},
|
|
371
|
+
CONVERSATION_REMOVED: {
|
|
372
|
+
target: 'ready',
|
|
373
|
+
actions: assign({
|
|
374
|
+
syncData: (context, event: any) => ({
|
|
375
|
+
...context.syncData,
|
|
376
|
+
conversations: (context.syncData?.conversations || []).filter(
|
|
377
|
+
(c: any) => c.id !== event.conversationId
|
|
378
|
+
),
|
|
379
|
+
}),
|
|
380
|
+
}),
|
|
381
|
+
},
|
|
382
|
+
OFFLINE: 'offline',
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
error: {
|
|
387
|
+
on: {
|
|
388
|
+
RETRY: {
|
|
389
|
+
target: 'loading',
|
|
390
|
+
cond: (context) => context.retryCount < 3,
|
|
391
|
+
},
|
|
392
|
+
RESET: 'uninitialized',
|
|
393
|
+
OFFLINE: 'offline',
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
offline: {
|
|
398
|
+
on: {
|
|
399
|
+
ONLINE: 'ready',
|
|
400
|
+
RESET: 'uninitialized',
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
// ============================================================================
|
|
408
|
+
// OFFLINE QUEUE STATE MACHINE
|
|
409
|
+
// ============================================================================
|
|
410
|
+
|
|
411
|
+
export const offlineQueueMachine = createMachine(
|
|
412
|
+
{
|
|
413
|
+
id: 'offlineQueue',
|
|
414
|
+
initial: 'idle',
|
|
415
|
+
context: {
|
|
416
|
+
conversationId: undefined,
|
|
417
|
+
messageId: undefined,
|
|
418
|
+
lastError: undefined,
|
|
419
|
+
retryCount: 0,
|
|
420
|
+
syncData: {},
|
|
421
|
+
},
|
|
422
|
+
states: {
|
|
423
|
+
idle: {
|
|
424
|
+
on: {
|
|
425
|
+
QUEUE_OPERATION: {
|
|
426
|
+
target: 'queued',
|
|
427
|
+
actions: assign({
|
|
428
|
+
syncData: (context, event: any) => ({
|
|
429
|
+
...context.syncData,
|
|
430
|
+
queue: [...(context.syncData?.queue || []), event.operation],
|
|
431
|
+
}),
|
|
432
|
+
}),
|
|
433
|
+
},
|
|
434
|
+
FLUSH: 'flushing',
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
queued: {
|
|
439
|
+
on: {
|
|
440
|
+
QUEUE_OPERATION: {
|
|
441
|
+
target: 'queued',
|
|
442
|
+
actions: assign({
|
|
443
|
+
syncData: (context, event: any) => ({
|
|
444
|
+
...context.syncData,
|
|
445
|
+
queue: [...(context.syncData?.queue || []), event.operation],
|
|
446
|
+
}),
|
|
447
|
+
}),
|
|
448
|
+
},
|
|
449
|
+
FLUSH: 'flushing',
|
|
450
|
+
CLEAR: {
|
|
451
|
+
target: 'idle',
|
|
452
|
+
actions: assign({
|
|
453
|
+
syncData: (context) => ({
|
|
454
|
+
...context.syncData,
|
|
455
|
+
queue: [],
|
|
456
|
+
}),
|
|
457
|
+
}),
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
flushing: {
|
|
463
|
+
on: {
|
|
464
|
+
FLUSH_SUCCESS: {
|
|
465
|
+
target: 'idle',
|
|
466
|
+
actions: assign({
|
|
467
|
+
syncData: (context) => ({
|
|
468
|
+
...context.syncData,
|
|
469
|
+
queue: [],
|
|
470
|
+
}),
|
|
471
|
+
}),
|
|
472
|
+
},
|
|
473
|
+
FLUSH_ERROR: {
|
|
474
|
+
target: 'error',
|
|
475
|
+
actions: assign({
|
|
476
|
+
lastError: (context, event: any) => event.error,
|
|
477
|
+
retryCount: (context) => context.retryCount + 1,
|
|
478
|
+
}),
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
after: {
|
|
482
|
+
30000: {
|
|
483
|
+
target: 'error',
|
|
484
|
+
actions: assign({
|
|
485
|
+
lastError: new Error('Flush timeout (30s)'),
|
|
486
|
+
}),
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
error: {
|
|
492
|
+
on: {
|
|
493
|
+
RETRY: {
|
|
494
|
+
target: 'flushing',
|
|
495
|
+
cond: (context) => context.retryCount < 5,
|
|
496
|
+
},
|
|
497
|
+
CLEAR: 'idle',
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
// ============================================================================
|
|
505
|
+
// CONFLICT RESOLUTION STATE MACHINE
|
|
506
|
+
// ============================================================================
|
|
507
|
+
|
|
508
|
+
export const conflictResolutionMachine = createMachine(
|
|
509
|
+
{
|
|
510
|
+
id: 'conflictResolution',
|
|
511
|
+
initial: 'idle',
|
|
512
|
+
context: {
|
|
513
|
+
conversationId: undefined,
|
|
514
|
+
messageId: undefined,
|
|
515
|
+
lastError: undefined,
|
|
516
|
+
retryCount: 0,
|
|
517
|
+
syncData: {},
|
|
518
|
+
},
|
|
519
|
+
states: {
|
|
520
|
+
idle: {
|
|
521
|
+
on: {
|
|
522
|
+
CONFLICT_DETECTED: 'resolving',
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
resolving: {
|
|
527
|
+
on: {
|
|
528
|
+
RESOLVE_SUCCESS: 'resolved',
|
|
529
|
+
RESOLVE_FAILED: 'error',
|
|
530
|
+
},
|
|
531
|
+
after: {
|
|
532
|
+
5000: {
|
|
533
|
+
target: 'error',
|
|
534
|
+
actions: assign({
|
|
535
|
+
lastError: new Error('Conflict resolution timeout (5s)'),
|
|
536
|
+
}),
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
resolved: {
|
|
542
|
+
on: {
|
|
543
|
+
CONTINUE: 'idle',
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
error: {
|
|
548
|
+
on: {
|
|
549
|
+
RETRY: 'resolving',
|
|
550
|
+
MANUAL_RESOLVE: 'resolving',
|
|
551
|
+
ABORT: 'idle',
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
}
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
// ============================================================================
|
|
559
|
+
// STATE MACHINE SELECTORS & UTILITIES
|
|
560
|
+
// ============================================================================
|
|
561
|
+
|
|
562
|
+
export function getStateDescription(state: string): string {
|
|
563
|
+
const descriptions: Record<string, string> = {
|
|
564
|
+
idle: 'Waiting for input',
|
|
565
|
+
loading: 'Loading data from server',
|
|
566
|
+
syncing: 'Syncing data with server',
|
|
567
|
+
synced: 'Data is synchronized',
|
|
568
|
+
error: 'Error occurred, will retry',
|
|
569
|
+
offline: 'Offline mode - operations queued',
|
|
570
|
+
creating: 'Creating message',
|
|
571
|
+
created: 'Message created, waiting for sync',
|
|
572
|
+
ready: 'Ready for operations',
|
|
573
|
+
uninitialized: 'Not yet initialized',
|
|
574
|
+
queued: 'Operations queued offline',
|
|
575
|
+
flushing: 'Sending queued operations',
|
|
576
|
+
resolving: 'Resolving data conflicts',
|
|
577
|
+
resolved: 'Conflicts resolved',
|
|
578
|
+
reconciling: 'Reconciling local and remote changes',
|
|
579
|
+
};
|
|
580
|
+
return descriptions[state] || state;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function isTerminalState(state: string): boolean {
|
|
584
|
+
return ['synced', 'resolved', 'ready'].includes(state);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function isErrorState(state: string): boolean {
|
|
588
|
+
return state === 'error' || state === 'offline';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function canRetry(state: string): boolean {
|
|
592
|
+
return state === 'error' || state === 'offline';
|
|
593
|
+
}
|