forked from ZyntariHQ/Invoisio
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdraft-sync.js
More file actions
103 lines (89 loc) · 3.43 KB
/
Copy pathdraft-sync.js
File metadata and controls
103 lines (89 loc) · 3.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
const RETRY_LIMIT = 3;
function parseTime(value) {
if (!value) return 0;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? 0 : date.getTime();
}
function normalizeDraft(draft = {}) {
return {
...draft,
id: draft.id || draft.localId || draft.remoteId,
baseRevision: draft.baseRevision ?? draft.base_rev ?? null,
localRevision: draft.localRevision ?? draft.revision ?? null,
remoteRevision: draft.remoteRevision ?? draft.serverRevision ?? null,
updatedAt: draft.updatedAt || draft.updated_at || null,
lastSyncAttemptAt: draft.lastSyncAttemptAt || draft.last_sync_attempt_at || null,
retryCount: Number(draft.retryCount ?? draft.retry_count ?? 0),
};
}
function hasLocalChanges(draft) {
if (draft.dirty === true || draft.isDirty === true || draft.status === 'unsynced') return true;
return draft.localRevision !== null && draft.localRevision !== draft.baseRevision;
}
function hasRemoteChanges(draft) {
return draft.remoteRevision !== null && draft.remoteRevision !== draft.baseRevision;
}
function isConflict(draft) {
return hasLocalChanges(draft) && hasRemoteChanges(draft) && draft.localRevision !== draft.remoteRevision;
}
function classifyDraftSync(draftInput) {
const draft = normalizeDraft(draftInput);
const failedRepeatedly = draft.retryCount >= RETRY_LIMIT;
if (draft.status === 'conflicted' || isConflict(draft)) {
return {
...draft,
syncStatus: 'conflicted',
severity: 'danger',
recoveryActions: ['keep-local-copy', 'use-server-copy', 'duplicate-and-review'],
message: 'Local and server versions both changed. Review both copies before syncing.',
};
}
if (failedRepeatedly || draft.status === 'failed') {
return {
...draft,
syncStatus: 'needs-recovery',
severity: 'warning',
recoveryActions: ['retry-sync', 'duplicate-and-review', 'discard-local-copy'],
message: 'Automatic sync failed repeatedly. Choose a recovery action when back online.',
};
}
if (hasLocalChanges(draft)) {
return {
...draft,
syncStatus: 'unsynced',
severity: 'info',
recoveryActions: ['sync-now', 'keep-editing-offline'],
message: 'Saved locally and waiting for a safe sync.',
};
}
return {
...draft,
syncStatus: 'synced',
severity: 'success',
recoveryActions: [],
message: 'Draft is up to date on this device and the server.',
};
}
function buildDraftSyncCenter(localDrafts = [], remoteDrafts = []) {
const remoteById = new Map((Array.isArray(remoteDrafts) ? remoteDrafts : []).map((draft) => [draft.id || draft.remoteId, draft]));
const drafts = (Array.isArray(localDrafts) ? localDrafts : []).map((localDraft) => {
const remoteDraft = remoteById.get(localDraft.id || localDraft.remoteId);
return classifyDraftSync({
...localDraft,
remoteRevision: remoteDraft?.revision ?? remoteDraft?.remoteRevision ?? localDraft.remoteRevision,
remoteUpdatedAt: remoteDraft?.updatedAt ?? remoteDraft?.updated_at,
});
}).sort((left, right) => parseTime(right.updatedAt) - parseTime(left.updatedAt));
const summary = drafts.reduce((counts, draft) => {
counts.total += 1;
counts[draft.syncStatus] = (counts[draft.syncStatus] || 0) + 1;
return counts;
}, { total: 0, synced: 0, unsynced: 0, conflicted: 0, 'needs-recovery': 0 });
return { summary, drafts };
}
module.exports = {
RETRY_LIMIT,
buildDraftSyncCenter,
classifyDraftSync,
isConflict,
};