Content merge
This commit is contained in:
parent
d258d1c9c4
commit
0280df2bbb
@ -1,7 +1,7 @@
|
|||||||
export default () => ({
|
export default () => ({
|
||||||
id: null,
|
id: null,
|
||||||
type: 'syncContent',
|
type: 'syncContent',
|
||||||
contentRevisions: {},
|
historyData: {},
|
||||||
syncLocationData: {},
|
syncLocationData: {},
|
||||||
updated: 0,
|
updated: 0,
|
||||||
});
|
});
|
||||||
|
@ -1,482 +0,0 @@
|
|||||||
import 'clunderscore';
|
|
||||||
import DiffMatchPatch from 'diff-match-patch';
|
|
||||||
|
|
||||||
var clDiffUtils = {
|
|
||||||
cloneObject: cloneObject,
|
|
||||||
offsetToPatch: offsetToPatch,
|
|
||||||
patchToOffset: patchToOffset,
|
|
||||||
serializeObject: serializeObject,
|
|
||||||
flattenContent: flattenContent,
|
|
||||||
makePatchableText: makePatchableText,
|
|
||||||
restoreDiscussionOffsets: restoreDiscussionOffsets,
|
|
||||||
makeContentChange: makeContentChange,
|
|
||||||
applyContentChanges: applyContentChanges,
|
|
||||||
getTextPatches: getTextPatches,
|
|
||||||
getObjectPatches: getObjectPatches,
|
|
||||||
quickPatch: quickPatch,
|
|
||||||
mergeObjects: mergeObjects,
|
|
||||||
mergeFlattenContent: mergeFlattenContent
|
|
||||||
}
|
|
||||||
|
|
||||||
var marker = '\uF111\uF222\uF333\uF444'
|
|
||||||
var DIFF_DELETE = -1
|
|
||||||
var DIFF_INSERT = 1
|
|
||||||
var DIFF_EQUAL = 0
|
|
||||||
var diffMatchPatch = new DiffMatchPatch() // eslint-disable-line new-cap
|
|
||||||
var diffMatchPatchStrict = new DiffMatchPatch() // eslint-disable-line new-cap
|
|
||||||
diffMatchPatchStrict.Match_Threshold = 0
|
|
||||||
diffMatchPatchStrict.Patch_DeleteThreshold = 0
|
|
||||||
var diffMatchPatchPermissive = new DiffMatchPatch() // eslint-disable-line new-cap
|
|
||||||
diffMatchPatchPermissive.Match_Distance = 999999999
|
|
||||||
|
|
||||||
function cloneObject (obj) {
|
|
||||||
return JSON.parse(JSON.stringify(obj))
|
|
||||||
}
|
|
||||||
|
|
||||||
function offsetToPatch (text, offset) {
|
|
||||||
var patch = diffMatchPatchPermissive.patch_make(text, [
|
|
||||||
[0, text.slice(0, offset)],
|
|
||||||
[1, marker],
|
|
||||||
[0, text.slice(offset)]
|
|
||||||
])[0]
|
|
||||||
var diffs = patch.diffs.cl_map(function (diff) {
|
|
||||||
if (!diff[0]) {
|
|
||||||
return diff[1]
|
|
||||||
} else if (diff[1] === marker) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
diffs: diffs,
|
|
||||||
length: patch.length1,
|
|
||||||
start: patch.start1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function patchToOffset (text, patch) {
|
|
||||||
var markersLength = 0
|
|
||||||
var diffs = patch.diffs.cl_map(function (diff) {
|
|
||||||
if (!diff) {
|
|
||||||
markersLength += marker.length
|
|
||||||
return [1, marker]
|
|
||||||
} else {
|
|
||||||
return [0, diff]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return diffMatchPatchPermissive.patch_apply([{
|
|
||||||
diffs: diffs,
|
|
||||||
length1: patch.length,
|
|
||||||
length2: patch.length + markersLength,
|
|
||||||
start1: patch.start,
|
|
||||||
start2: patch.start
|
|
||||||
}], text)[0].indexOf(marker)
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenObject (obj) {
|
|
||||||
return obj.cl_reduce(function (result, value, key) {
|
|
||||||
result[key] = value[1]
|
|
||||||
return result
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenContent (content) {
|
|
||||||
var result = ({}).cl_extend(content)
|
|
||||||
result.properties = flattenObject(content.properties)
|
|
||||||
result.discussions = flattenObject(content.discussions)
|
|
||||||
result.comments = flattenObject(content.comments)
|
|
||||||
result.text = content.text.cl_reduce(function (text, item) {
|
|
||||||
switch (item.type) {
|
|
||||||
case 'discussion':
|
|
||||||
if (result.discussions[item.id]) {
|
|
||||||
result.discussions[item.id][item.name] = text.length
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
default:
|
|
||||||
return text + item[1]
|
|
||||||
}
|
|
||||||
}, '')
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTextPatches (oldText, newText) {
|
|
||||||
var diffs = diffMatchPatch.diff_main(oldText, newText)
|
|
||||||
diffMatchPatch.diff_cleanupEfficiency(diffs)
|
|
||||||
var patches = []
|
|
||||||
var startOffset = 0
|
|
||||||
diffs.cl_each(function (change) {
|
|
||||||
var changeType = change[0]
|
|
||||||
var changeText = change[1]
|
|
||||||
switch (changeType) {
|
|
||||||
case DIFF_EQUAL:
|
|
||||||
startOffset += changeText.length
|
|
||||||
break
|
|
||||||
case DIFF_DELETE:
|
|
||||||
changeText && patches.push({
|
|
||||||
o: startOffset,
|
|
||||||
d: changeText
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case DIFF_INSERT:
|
|
||||||
changeText && patches.push({
|
|
||||||
o: startOffset,
|
|
||||||
a: changeText
|
|
||||||
})
|
|
||||||
startOffset += changeText.length
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return patches.length ? patches : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function getObjectPatches (oldObject, newObjects) {
|
|
||||||
var valueHash = Object.create(null)
|
|
||||||
var valueArray = []
|
|
||||||
oldObject = hashObject(oldObject, valueHash, valueArray)
|
|
||||||
newObjects = hashObject(newObjects, valueHash, valueArray)
|
|
||||||
var diffs = diffMatchPatch.diff_main(oldObject, newObjects)
|
|
||||||
var patches = []
|
|
||||||
diffs.cl_each(function (change) {
|
|
||||||
var changeType = change[0]
|
|
||||||
var changeHash = change[1]
|
|
||||||
if (changeType === DIFF_EQUAL) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
changeHash.split('').cl_each(function (objHash) {
|
|
||||||
var obj = valueArray[objHash.charCodeAt(0)]
|
|
||||||
var patch = {
|
|
||||||
k: obj[0]
|
|
||||||
}
|
|
||||||
patch[changeType === DIFF_DELETE ? 'd' : 'a'] = obj[1]
|
|
||||||
patches.push(patch)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return patches.length ? patches : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function makePatchableText (content, markerKeys, markerIdxMap) {
|
|
||||||
var markers = []
|
|
||||||
// Sort keys to have predictable marker positions, in case of same offset
|
|
||||||
var discussionKeys = Object.keys(content.discussions).sort()
|
|
||||||
discussionKeys.cl_each(function (discussionId) {
|
|
||||||
function addMarker (offsetName) {
|
|
||||||
var markerKey = discussionId + offsetName
|
|
||||||
if (discussion[offsetName] !== undefined) {
|
|
||||||
var idx = markerIdxMap[markerKey]
|
|
||||||
if (idx === undefined) {
|
|
||||||
idx = markerKeys.length
|
|
||||||
markerIdxMap[markerKey] = idx
|
|
||||||
markerKeys.push({
|
|
||||||
id: discussionId,
|
|
||||||
offsetName: offsetName
|
|
||||||
})
|
|
||||||
}
|
|
||||||
markers.push({
|
|
||||||
idx: idx,
|
|
||||||
offset: discussion[offsetName]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var discussion = content.discussions[discussionId]
|
|
||||||
if (discussion.offset0 === discussion.offset1) {
|
|
||||||
// Remove discussion offsets if markers are at the same position
|
|
||||||
discussion.offset0 = discussion.offset1 = undefined
|
|
||||||
} else {
|
|
||||||
addMarker('offset0')
|
|
||||||
addMarker('offset1')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var lastOffset = 0
|
|
||||||
var result = ''
|
|
||||||
markers
|
|
||||||
.sort(function (marker1, marker2) {
|
|
||||||
return marker1.offset - marker2.offset
|
|
||||||
})
|
|
||||||
.cl_each(function (marker) {
|
|
||||||
result +=
|
|
||||||
content.text.slice(lastOffset, marker.offset) +
|
|
||||||
String.fromCharCode(0xe000 + marker.idx) // Use a character from the private use area
|
|
||||||
lastOffset = marker.offset
|
|
||||||
})
|
|
||||||
return result + content.text.slice(lastOffset)
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripDiscussionOffsets (objectMap) {
|
|
||||||
return objectMap.cl_reduce(function (result, object, id) {
|
|
||||||
result[id] = {
|
|
||||||
text: object.text
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreDiscussionOffsets (content, markerKeys) {
|
|
||||||
var len = content.text.length
|
|
||||||
var maxIdx = markerKeys.length
|
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
var idx = content.text.charCodeAt(i) - 0xe000
|
|
||||||
if (idx >= 0 && idx < maxIdx) {
|
|
||||||
var markerKey = markerKeys[idx]
|
|
||||||
content.text = content.text.slice(0, i) + content.text.slice(i + 1)
|
|
||||||
var discussion = content.discussions[markerKey.id]
|
|
||||||
if (discussion) {
|
|
||||||
discussion[markerKey.offsetName] = i
|
|
||||||
}
|
|
||||||
i-- // We just removed the current character, we may have multiple markers with same offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeContentChange (oldContent, newContent) {
|
|
||||||
var markerKeys = []
|
|
||||||
var markerIdxMap = Object.create(null)
|
|
||||||
var oldText = makePatchableText(oldContent, markerKeys, markerIdxMap)
|
|
||||||
var newText = makePatchableText(newContent, markerKeys, markerIdxMap)
|
|
||||||
var textPatches = getTextPatches(oldText, newText)
|
|
||||||
textPatches && textPatches.cl_each(function (patch) {
|
|
||||||
// If markers are present, replace changeText with an array of text and markers
|
|
||||||
var changeText = patch.a || patch.d
|
|
||||||
var textItems = []
|
|
||||||
var lastItem = ''
|
|
||||||
var len = changeText.length
|
|
||||||
var maxIdx = markerKeys.length
|
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
var idx = changeText.charCodeAt(i) - 0xe000
|
|
||||||
if (idx >= 0 && idx < maxIdx) {
|
|
||||||
var markerKey = markerKeys[idx]
|
|
||||||
lastItem.length && textItems.push(lastItem)
|
|
||||||
textItems.push({
|
|
||||||
type: 'discussion',
|
|
||||||
name: markerKey.offsetName,
|
|
||||||
id: markerKey.id
|
|
||||||
})
|
|
||||||
lastItem = ''
|
|
||||||
} else {
|
|
||||||
lastItem += changeText[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (textItems.length) {
|
|
||||||
lastItem.length && textItems.push(lastItem)
|
|
||||||
if (patch.a) {
|
|
||||||
patch.a = textItems
|
|
||||||
} else {
|
|
||||||
patch.d = textItems
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
var propertiesPatches = getObjectPatches(oldContent.properties, newContent.properties)
|
|
||||||
var discussionsPatches = getObjectPatches(
|
|
||||||
stripDiscussionOffsets(oldContent.discussions),
|
|
||||||
stripDiscussionOffsets(newContent.discussions)
|
|
||||||
)
|
|
||||||
var commentsPatches = getObjectPatches(oldContent.comments, newContent.comments)
|
|
||||||
if (textPatches || propertiesPatches || discussionsPatches || commentsPatches) {
|
|
||||||
return {
|
|
||||||
text: textPatches,
|
|
||||||
properties: propertiesPatches,
|
|
||||||
discussions: discussionsPatches,
|
|
||||||
comments: commentsPatches
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyContentChanges (content, contentChanges, isBackward) {
|
|
||||||
function applyObjectPatches (obj, patches) {
|
|
||||||
if (patches) {
|
|
||||||
patches.cl_each(function (patch) {
|
|
||||||
if (!patch.a ^ !isBackward) {
|
|
||||||
obj[patch.k] = patch.a || patch.d
|
|
||||||
} else {
|
|
||||||
delete obj[patch.k]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var markerKeys = []
|
|
||||||
var markerIdxMap = Object.create(null)
|
|
||||||
var result = {
|
|
||||||
text: makePatchableText(content, markerKeys, markerIdxMap),
|
|
||||||
properties: cloneObject(content.properties),
|
|
||||||
discussions: stripDiscussionOffsets(content.discussions),
|
|
||||||
comments: cloneObject(content.comments)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentChanges.cl_each(function (contentChange) {
|
|
||||||
var textPatches = contentChange.text || []
|
|
||||||
if (isBackward) {
|
|
||||||
textPatches = textPatches.slice().reverse()
|
|
||||||
}
|
|
||||||
result.text = textPatches.cl_reduce(function (text, patch) {
|
|
||||||
var isAdd = !patch.a ^ !isBackward
|
|
||||||
var textChanges = patch.a || patch.d || ''
|
|
||||||
// When no marker is present, textChanges is a string
|
|
||||||
if (typeof textChanges === 'string') {
|
|
||||||
textChanges = [textChanges]
|
|
||||||
}
|
|
||||||
var textChange = textChanges.cl_map(function (textChange) {
|
|
||||||
if (!textChange.type) {
|
|
||||||
// textChange is a string
|
|
||||||
return textChange
|
|
||||||
}
|
|
||||||
// textChange is a marker
|
|
||||||
var markerKey = textChange.id + textChange.name
|
|
||||||
var idx = markerIdxMap[markerKey]
|
|
||||||
if (idx === undefined) {
|
|
||||||
idx = markerKeys.length
|
|
||||||
markerIdxMap[markerKey] = idx
|
|
||||||
markerKeys.push({
|
|
||||||
id: textChange.id,
|
|
||||||
offsetName: textChange.name
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return String.fromCharCode(0xe000 + idx)
|
|
||||||
}).join('')
|
|
||||||
if (!textChange) {
|
|
||||||
return text
|
|
||||||
} else if (isAdd) {
|
|
||||||
return text.slice(0, patch.o).concat(textChange).concat(text.slice(patch.o))
|
|
||||||
} else {
|
|
||||||
return text.slice(0, patch.o).concat(text.slice(patch.o + textChange.length))
|
|
||||||
}
|
|
||||||
}, result.text)
|
|
||||||
|
|
||||||
applyObjectPatches(result.properties, contentChange.properties)
|
|
||||||
applyObjectPatches(result.discussions, contentChange.discussions)
|
|
||||||
applyObjectPatches(result.comments, contentChange.comments)
|
|
||||||
})
|
|
||||||
|
|
||||||
restoreDiscussionOffsets(result, markerKeys)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeObject (obj) {
|
|
||||||
return JSON.stringify(obj, function (key, value) {
|
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
|
||||||
? Object.keys(value).sort().cl_reduce(function (sorted, key) {
|
|
||||||
sorted[key] = value[key]
|
|
||||||
return sorted
|
|
||||||
}, {})
|
|
||||||
: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashArray (arr, valueHash, valueArray) {
|
|
||||||
var hash = []
|
|
||||||
arr.cl_each(function (obj) {
|
|
||||||
var serializedObj = serializeObject(obj)
|
|
||||||
var objHash = valueHash[serializedObj]
|
|
||||||
if (objHash === undefined) {
|
|
||||||
objHash = valueArray.length
|
|
||||||
valueArray.push(obj)
|
|
||||||
valueHash[serializedObj] = objHash
|
|
||||||
}
|
|
||||||
hash.push(objHash)
|
|
||||||
})
|
|
||||||
return String.fromCharCode.apply(null, hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hashObject (obj, valueHash, valueArray) {
|
|
||||||
return hashArray(Object.keys(obj || {}).sort().cl_map(function (key) {
|
|
||||||
return [key, obj[key]]
|
|
||||||
}), valueHash, valueArray)
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeText (oldText, newText, serverText) {
|
|
||||||
var diffs = diffMatchPatch.diff_main(oldText, newText)
|
|
||||||
diffMatchPatch.diff_cleanupSemantic(diffs)
|
|
||||||
var patches = diffMatchPatch.patch_make(oldText, diffs)
|
|
||||||
var patchResult = diffMatchPatch.patch_apply(patches, serverText)
|
|
||||||
if (!patchResult[1]
|
|
||||||
.cl_some(function (changeApplied) {
|
|
||||||
return !changeApplied
|
|
||||||
})) {
|
|
||||||
return patchResult[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
diffs = diffMatchPatchStrict.diff_main(patchResult[0], newText)
|
|
||||||
diffMatchPatch.diff_cleanupSemantic(diffs)
|
|
||||||
return diffs.cl_map(function (diff) {
|
|
||||||
return diff[1]
|
|
||||||
}).join('')
|
|
||||||
}
|
|
||||||
|
|
||||||
function quickPatch (oldStr, newStr, destStr, strict) {
|
|
||||||
var dmp = strict ? diffMatchPatchStrict : diffMatchPatch
|
|
||||||
var diffs = dmp.diff_main(oldStr, newStr)
|
|
||||||
var patches = dmp.patch_make(oldStr, diffs)
|
|
||||||
var patchResult = dmp.patch_apply(patches, destStr)
|
|
||||||
return patchResult[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeObjects (oldObject, newObject, serverObject) {
|
|
||||||
var mergedObject = ({}).cl_extend(newObject).cl_extend(serverObject)
|
|
||||||
mergedObject.cl_each(function (value, key) {
|
|
||||||
if (!oldObject[key]) {
|
|
||||||
return // There might be conflict, keep the server value
|
|
||||||
}
|
|
||||||
var newValue = newObject[key] && serializeObject(newObject[key])
|
|
||||||
var serverValue = serverObject[key] && serializeObject(serverObject[key])
|
|
||||||
if (newValue === serverValue) {
|
|
||||||
return // no conflict
|
|
||||||
}
|
|
||||||
var oldValue = serializeObject(oldObject[key])
|
|
||||||
if (oldValue !== newValue && !serverValue) {
|
|
||||||
return // Removed on server but changed on client
|
|
||||||
}
|
|
||||||
if (oldValue !== serverValue && !newValue) {
|
|
||||||
return // Removed on client but changed on server
|
|
||||||
}
|
|
||||||
if (oldValue !== newValue && oldValue === serverValue) {
|
|
||||||
// Take the client value
|
|
||||||
if (!newValue) {
|
|
||||||
delete mergedObject[key]
|
|
||||||
} else {
|
|
||||||
mergedObject[key] = newObject[key]
|
|
||||||
}
|
|
||||||
} else if (oldValue !== serverValue && oldValue === newValue) {
|
|
||||||
// Take the server value
|
|
||||||
if (!serverValue) {
|
|
||||||
delete mergedObject[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Take the server value otherwise
|
|
||||||
})
|
|
||||||
return cloneObject(mergedObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeFlattenContent (oldContent, newContent, serverContent) {
|
|
||||||
var markerKeys = []
|
|
||||||
var markerIdxMap = Object.create(null)
|
|
||||||
var oldText = makePatchableText(oldContent, markerKeys, markerIdxMap)
|
|
||||||
var serverText = makePatchableText(serverContent, markerKeys, markerIdxMap)
|
|
||||||
var localText = makePatchableText(newContent, markerKeys, markerIdxMap)
|
|
||||||
var isServerTextChanges = oldText !== serverText
|
|
||||||
var isTextSynchronized = serverText === localText
|
|
||||||
|
|
||||||
var result = {
|
|
||||||
text: isTextSynchronized || !isServerTextChanges
|
|
||||||
? localText
|
|
||||||
: mergeText(oldText, serverText, localText),
|
|
||||||
properties: mergeObjects(
|
|
||||||
oldContent.properties,
|
|
||||||
newContent.properties,
|
|
||||||
serverContent.properties
|
|
||||||
),
|
|
||||||
discussions: mergeObjects(
|
|
||||||
stripDiscussionOffsets(oldContent.discussions),
|
|
||||||
stripDiscussionOffsets(newContent.discussions),
|
|
||||||
stripDiscussionOffsets(serverContent.discussions)
|
|
||||||
),
|
|
||||||
comments: mergeObjects(
|
|
||||||
oldContent.comments,
|
|
||||||
newContent.comments,
|
|
||||||
serverContent.comments
|
|
||||||
)
|
|
||||||
}
|
|
||||||
restoreDiscussionOffsets(result, markerKeys)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export default clDiffUtils;
|
|
204
src/services/diffUtils.js
Normal file
204
src/services/diffUtils.js
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import DiffMatchPatch from 'diff-match-patch';
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
|
const diffMatchPatch = new DiffMatchPatch();
|
||||||
|
const diffMatchPatchStrict = new DiffMatchPatch();
|
||||||
|
diffMatchPatchStrict.Match_Threshold = 0;
|
||||||
|
diffMatchPatchStrict.Patch_DeleteThreshold = 0;
|
||||||
|
const diffMatchPatchPermissive = new DiffMatchPatch();
|
||||||
|
diffMatchPatchPermissive.Match_Distance = 999999999;
|
||||||
|
|
||||||
|
function makePatchableText(content, markerKeys, markerIdxMap) {
|
||||||
|
const markers = [];
|
||||||
|
// Sort keys to have predictable marker positions in case of same offset
|
||||||
|
const discussionKeys = Object.keys(content.discussions).sort();
|
||||||
|
discussionKeys.forEach((discussionId) => {
|
||||||
|
const discussion = content.discussions[discussionId];
|
||||||
|
|
||||||
|
function addMarker(offsetName) {
|
||||||
|
const markerKey = discussionId + offsetName;
|
||||||
|
if (discussion[offsetName] !== undefined) {
|
||||||
|
let idx = markerIdxMap[markerKey];
|
||||||
|
if (idx === undefined) {
|
||||||
|
idx = markerKeys.length;
|
||||||
|
markerIdxMap[markerKey] = idx;
|
||||||
|
markerKeys.push({
|
||||||
|
id: discussionId,
|
||||||
|
offsetName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
markers.push({
|
||||||
|
idx,
|
||||||
|
offset: discussion[offsetName],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discussion.offset0 === discussion.offset1) {
|
||||||
|
// Remove discussion offsets if markers are at the same position
|
||||||
|
discussion.offset0 = undefined;
|
||||||
|
discussion.offset1 = undefined;
|
||||||
|
} else {
|
||||||
|
addMarker('offset0');
|
||||||
|
addMarker('offset1');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastOffset = 0;
|
||||||
|
let result = '';
|
||||||
|
markers
|
||||||
|
.sort((marker1, marker2) => marker1.offset - marker2.offset)
|
||||||
|
.forEach((marker) => {
|
||||||
|
result +=
|
||||||
|
content.text.slice(lastOffset, marker.offset) +
|
||||||
|
String.fromCharCode(0xe000 + marker.idx); // Use a character from the private use area
|
||||||
|
lastOffset = marker.offset;
|
||||||
|
});
|
||||||
|
return result + content.text.slice(lastOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripDiscussionOffsets(objectMap) {
|
||||||
|
const result = {};
|
||||||
|
Object.keys(objectMap).forEach((id) => {
|
||||||
|
result[id] = {
|
||||||
|
text: objectMap[id].text,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreDiscussionOffsets(content, markerKeys) {
|
||||||
|
const len = content.text.length;
|
||||||
|
const maxIdx = markerKeys.length;
|
||||||
|
for (let i = 0; i < len; i += 1) {
|
||||||
|
const idx = content.text.charCodeAt(i) - 0xe000;
|
||||||
|
if (idx >= 0 && idx < maxIdx) {
|
||||||
|
const markerKey = markerKeys[idx];
|
||||||
|
content.text = content.text.slice(0, i) + content.text.slice(i + 1);
|
||||||
|
const discussion = content.discussions[markerKey.id];
|
||||||
|
if (discussion) {
|
||||||
|
discussion[markerKey.offsetName] = i;
|
||||||
|
}
|
||||||
|
// We just removed the current character, we may have multiple markers with same offset
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeObject(obj) {
|
||||||
|
if (!obj) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return JSON.stringify(obj, (key, value) => {
|
||||||
|
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return Object.keys(value).sort().reduce((sorted, valueKey) => {
|
||||||
|
sorted[valueKey] = value[valueKey];
|
||||||
|
return sorted;
|
||||||
|
}, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeText(oldText, newText, serverText) {
|
||||||
|
let diffs = diffMatchPatch.diff_main(oldText, newText);
|
||||||
|
diffMatchPatch.diff_cleanupSemantic(diffs);
|
||||||
|
const patches = diffMatchPatch.patch_make(oldText, diffs);
|
||||||
|
const patchResult = diffMatchPatch.patch_apply(patches, serverText);
|
||||||
|
if (!patchResult[1].some(changeApplied => !changeApplied)) {
|
||||||
|
return patchResult[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
diffs = diffMatchPatchStrict.diff_main(patchResult[0], newText);
|
||||||
|
diffMatchPatch.diff_cleanupSemantic(diffs);
|
||||||
|
return diffs.map(diff => diff[1]).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickPatch(oldStr, newStr, destStr, strict) {
|
||||||
|
const dmp = strict ? diffMatchPatchStrict : diffMatchPatch;
|
||||||
|
const diffs = dmp.diff_main(oldStr, newStr);
|
||||||
|
const patches = dmp.patch_make(oldStr, diffs);
|
||||||
|
const patchResult = dmp.patch_apply(patches, destStr);
|
||||||
|
return patchResult[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeValue(oldValue, newValue, serverValue) {
|
||||||
|
if (!oldValue) {
|
||||||
|
return serverValue; // There might be conflict, keep the server value
|
||||||
|
}
|
||||||
|
const newSerializedValue = serializeObject(newValue);
|
||||||
|
const serverSerializedValue = serializeObject(serverValue);
|
||||||
|
if (newSerializedValue === serverSerializedValue) {
|
||||||
|
return serverValue; // no conflict
|
||||||
|
}
|
||||||
|
const oldSerializedValue = serializeObject(oldValue);
|
||||||
|
if (oldSerializedValue !== newSerializedValue && !serverValue) {
|
||||||
|
return newValue; // Removed on server but changed on client
|
||||||
|
}
|
||||||
|
if (oldSerializedValue !== serverSerializedValue && !newValue) {
|
||||||
|
return serverValue; // Removed on client but changed on server
|
||||||
|
}
|
||||||
|
if (oldSerializedValue !== newSerializedValue && oldSerializedValue === serverSerializedValue) {
|
||||||
|
return newValue; // Take the client value
|
||||||
|
}
|
||||||
|
return serverValue; // Take the server value otherwise
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeObjects(oldObject, newObject, serverObject) {
|
||||||
|
const mergedObject = {};
|
||||||
|
Object.keys({
|
||||||
|
...newObject,
|
||||||
|
...serverObject,
|
||||||
|
}).forEach((key) => {
|
||||||
|
const mergedValue = mergeValue(oldObject[key], newObject[key], serverObject[key]);
|
||||||
|
if (mergedValue != null) {
|
||||||
|
mergedObject[key] = mergedValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return utils.deepCopy(mergedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeContent(oldContent, newContent, serverContent) {
|
||||||
|
const markerKeys = [];
|
||||||
|
const markerIdxMap = Object.create(null);
|
||||||
|
const oldText = makePatchableText(oldContent, markerKeys, markerIdxMap);
|
||||||
|
const serverText = makePatchableText(serverContent, markerKeys, markerIdxMap);
|
||||||
|
const localText = makePatchableText(newContent, markerKeys, markerIdxMap);
|
||||||
|
const isServerTextChanges = oldText !== serverText;
|
||||||
|
const isTextSynchronized = serverText === localText;
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
text: isTextSynchronized || !isServerTextChanges
|
||||||
|
? localText
|
||||||
|
: mergeText(oldText, serverText, localText),
|
||||||
|
properties: mergeValue(
|
||||||
|
oldContent.properties,
|
||||||
|
newContent.properties,
|
||||||
|
serverContent.properties,
|
||||||
|
),
|
||||||
|
discussions: mergeObjects(
|
||||||
|
stripDiscussionOffsets(oldContent.discussions),
|
||||||
|
stripDiscussionOffsets(newContent.discussions),
|
||||||
|
stripDiscussionOffsets(serverContent.discussions),
|
||||||
|
),
|
||||||
|
comments: mergeObjects(
|
||||||
|
oldContent.comments,
|
||||||
|
newContent.comments,
|
||||||
|
serverContent.comments,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
restoreDiscussionOffsets(result, markerKeys);
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
serializeObject,
|
||||||
|
makePatchableText,
|
||||||
|
restoreDiscussionOffsets,
|
||||||
|
applyContentChanges,
|
||||||
|
getTextPatches,
|
||||||
|
getObjectPatches,
|
||||||
|
quickPatch,
|
||||||
|
mergeObjects,
|
||||||
|
mergeContent,
|
||||||
|
};
|
@ -23,25 +23,25 @@ const tokenExpirationMargin = 10 * 60 * 1000; // 10 min
|
|||||||
// ],
|
// ],
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const request = (googleToken, options) => utils.request({
|
const request = (token, options) => utils.request({
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
...options.headers,
|
...options.headers,
|
||||||
Authorization: `Bearer ${googleToken.accessToken}`,
|
Authorization: `Bearer ${token.accessToken}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
startOauth2(scopes, sub = null, silent = false) {
|
startOauth2(scopes, sub = null, silent = false) {
|
||||||
return utils.startOauth2(
|
return utils.startOauth2(
|
||||||
'https://accounts.google.com/o/oauth2/v2/auth', {
|
'https://accounts.google.com/o/oauth2/v2/auth', {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
response_type: 'token',
|
response_type: 'token',
|
||||||
scope: scopes.join(' '),
|
scope: scopes.join(' '),
|
||||||
hd: appsDomain,
|
hd: appsDomain,
|
||||||
login_hint: sub,
|
login_hint: sub,
|
||||||
prompt: silent ? 'none' : null,
|
prompt: silent ? 'none' : null,
|
||||||
}, silent)
|
}, silent)
|
||||||
// Call the tokeninfo endpoint
|
// Call the tokeninfo endpoint
|
||||||
.then(data => utils.request({
|
.then(data => utils.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -68,28 +68,28 @@ export default {
|
|||||||
};
|
};
|
||||||
}))
|
}))
|
||||||
// Call the tokeninfo endpoint
|
// Call the tokeninfo endpoint
|
||||||
.then(googleToken => request(googleToken, {
|
.then(token => request(token, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: 'https://www.googleapis.com/plus/v1/people/me',
|
url: 'https://www.googleapis.com/plus/v1/people/me',
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
// Add name to googleToken
|
// Add name to token
|
||||||
googleToken.name = res.body.displayName;
|
token.name = res.body.displayName;
|
||||||
const existingToken = store.getters['data/googleTokens'][googleToken.sub];
|
const existingToken = store.getters['data/googleTokens'][token.sub];
|
||||||
if (existingToken) {
|
if (existingToken) {
|
||||||
if (!sub) {
|
if (!sub) {
|
||||||
throw new Error('Google account already linked.');
|
throw new Error('Google account already linked.');
|
||||||
}
|
}
|
||||||
// Add isLogin and nextPageToken to googleToken
|
// Add isLogin and nextPageToken to token
|
||||||
googleToken.isLogin = existingToken.isLogin;
|
token.isLogin = existingToken.isLogin;
|
||||||
googleToken.nextPageToken = existingToken.nextPageToken;
|
token.nextPageToken = existingToken.nextPageToken;
|
||||||
}
|
}
|
||||||
// Add googleToken to googleTokens
|
// Add token to googleTokens
|
||||||
store.dispatch('data/setGoogleToken', googleToken);
|
store.dispatch('data/setGoogleToken', token);
|
||||||
return googleToken;
|
return token;
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
refreshToken(scopes, googleToken) {
|
refreshToken(scopes, token) {
|
||||||
const sub = googleToken.sub;
|
const sub = token.sub;
|
||||||
const lastToken = store.getters['data/googleTokens'][sub];
|
const lastToken = store.getters['data/googleTokens'][sub];
|
||||||
const mergedScopes = [...new Set([
|
const mergedScopes = [...new Set([
|
||||||
...scopes,
|
...scopes,
|
||||||
@ -115,9 +115,9 @@ export default {
|
|||||||
.catch(() => this.startOauth2(mergedScopes, sub));
|
.catch(() => this.startOauth2(mergedScopes, sub));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getChanges(googleToken) {
|
getChanges(token) {
|
||||||
let changes = [];
|
let changes = [];
|
||||||
return this.refreshToken(['https://www.googleapis.com/auth/drive.appdata'], googleToken)
|
return this.refreshToken(['https://www.googleapis.com/auth/drive.appdata'], token)
|
||||||
.then((refreshedToken) => {
|
.then((refreshedToken) => {
|
||||||
const getPage = (pageToken = '1') => request(refreshedToken, {
|
const getPage = (pageToken = '1') => request(refreshedToken, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -158,8 +158,8 @@ export default {
|
|||||||
return getPage(refreshedToken.nextPageToken);
|
return getPage(refreshedToken.nextPageToken);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updateNextPageToken(googleToken, changes) {
|
updateNextPageToken(token, changes) {
|
||||||
const lastToken = store.getters['data/googleTokens'][googleToken.sub];
|
const lastToken = store.getters['data/googleTokens'][token.sub];
|
||||||
if (changes.nextPageToken !== lastToken.nextPageToken) {
|
if (changes.nextPageToken !== lastToken.nextPageToken) {
|
||||||
store.dispatch('data/setGoogleToken', {
|
store.dispatch('data/setGoogleToken', {
|
||||||
...lastToken,
|
...lastToken,
|
||||||
@ -167,8 +167,8 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
saveItem(googleToken, item, syncData, ifNotTooLate = cb => res => cb(res)) {
|
saveItem(token, item, syncData, ifNotTooLate = cb => res => cb(res)) {
|
||||||
return this.refreshToken(['https://www.googleapis.com/auth/drive.appdata'], googleToken)
|
return this.refreshToken(['https://www.googleapis.com/auth/drive.appdata'], token)
|
||||||
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
||||||
.then(ifNotTooLate((refreshedToken) => {
|
.then(ifNotTooLate((refreshedToken) => {
|
||||||
const options = {
|
const options = {
|
||||||
@ -229,12 +229,22 @@ export default {
|
|||||||
}));
|
}));
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
removeItem(googleToken, syncData, ifNotTooLate = cb => res => cb(res)) {
|
removeItem(token, syncData, ifNotTooLate = cb => res => cb(res)) {
|
||||||
return this.refreshToken(['https://www.googleapis.com/auth/drive.appdata'], googleToken)
|
return this.refreshToken(['https://www.googleapis.com/auth/drive.appdata'], token)
|
||||||
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
||||||
.then(ifNotTooLate(refreshedToken => request(refreshedToken, {
|
.then(ifNotTooLate(refreshedToken => request(refreshedToken, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: `https://www.googleapis.com/drive/v3/files/${syncData.id}`,
|
url: `https://www.googleapis.com/drive/v3/files/${syncData.id}`,
|
||||||
})).then(() => syncData));
|
})).then(() => syncData));
|
||||||
},
|
},
|
||||||
|
downloadFile(refreshedToken, id) {
|
||||||
|
return request(refreshedToken, {
|
||||||
|
method: 'GET',
|
||||||
|
url: `https://www.googleapis.com/drive/v3/files/${id}?alt=media`,
|
||||||
|
}).then(res => res.body);
|
||||||
|
},
|
||||||
|
downloadAppDataFile(token, id) {
|
||||||
|
return this.refreshToken(['https://www.googleapis.com/auth/drive.appdata'], token)
|
||||||
|
.then(refreshedToken => this.downloadFile(refreshedToken, id));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -86,16 +86,11 @@ utils.types.forEach((type) => {
|
|||||||
updatedMap[type] = Object.create(null);
|
updatedMap[type] = Object.create(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
function isContentType(type) {
|
const contentTypes = {
|
||||||
switch (type) {
|
content: true,
|
||||||
case 'content':
|
contentState: true,
|
||||||
case 'contentState':
|
syncContent: true,
|
||||||
case 'syncContent':
|
};
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
lastTx: 0,
|
lastTx: 0,
|
||||||
@ -171,8 +166,8 @@ export default {
|
|||||||
Object.keys(this.updatedMap).forEach((type) => {
|
Object.keys(this.updatedMap).forEach((type) => {
|
||||||
// Remove this type only if file is deleted
|
// Remove this type only if file is deleted
|
||||||
let checker = cb => id => !storeItemMap[id] && cb(id);
|
let checker = cb => id => !storeItemMap[id] && cb(id);
|
||||||
if (isContentType(type)) {
|
if (contentTypes[type]) {
|
||||||
// For content types, remove only if file is deleted
|
// For content types, remove item only if file is deleted
|
||||||
checker = cb => (id) => {
|
checker = cb => (id) => {
|
||||||
if (!storeItemMap[id]) {
|
if (!storeItemMap[id]) {
|
||||||
const [fileId] = id.split('/');
|
const [fileId] = id.split('/');
|
||||||
@ -227,7 +222,7 @@ export default {
|
|||||||
// DB item is different from the corresponding store item
|
// DB item is different from the corresponding store item
|
||||||
this.updatedMap[dbItem.type][dbItem.id] = dbItem.updated;
|
this.updatedMap[dbItem.type][dbItem.id] = dbItem.updated;
|
||||||
// Update content only if it exists in the store
|
// Update content only if it exists in the store
|
||||||
if (existingStoreItem || !isContentType(dbItem.type)) {
|
if (existingStoreItem || !contentTypes[dbItem.type]) {
|
||||||
// Put item in the store
|
// Put item in the store
|
||||||
store.commit(`${dbItem.type}/setItem`, dbItem);
|
store.commit(`${dbItem.type}/setItem`, dbItem);
|
||||||
storeItemMap[dbItem.id] = dbItem;
|
storeItemMap[dbItem.id] = dbItem;
|
||||||
@ -236,9 +231,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve an item from the DB.
|
* Retrieve an item from the DB and put it in the store.
|
||||||
*/
|
*/
|
||||||
retrieveItem(id) {
|
loadItem(id) {
|
||||||
// Check if item is in the store
|
// Check if item is in the store
|
||||||
const itemInStore = store.getters.allItemMap[id];
|
const itemInStore = store.getters.allItemMap[id];
|
||||||
if (itemInStore) {
|
if (itemInStore) {
|
||||||
@ -259,11 +254,30 @@ export default {
|
|||||||
this.updatedMap[dbItem.type][dbItem.id] = dbItem.updated;
|
this.updatedMap[dbItem.type][dbItem.id] = dbItem.updated;
|
||||||
// Put item in the store
|
// Put item in the store
|
||||||
store.commit(`${dbItem.type}/setItem`, dbItem);
|
store.commit(`${dbItem.type}/setItem`, dbItem);
|
||||||
// Use deepCopy to freeze item
|
|
||||||
resolve(dbItem);
|
resolve(dbItem);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, () => onError());
|
}, () => onError());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unload from the store contents that haven't been opened recently
|
||||||
|
*/
|
||||||
|
unloadContents() {
|
||||||
|
const lastOpenedFileIds = store.getters['data/lastOpenedIds']
|
||||||
|
.slice(0, 10).reduce((result, id) => {
|
||||||
|
result[id] = true;
|
||||||
|
return result;
|
||||||
|
}, {});
|
||||||
|
Object.keys(contentTypes).forEach((type) => {
|
||||||
|
store.getters(`${type}/items`).forEach((item) => {
|
||||||
|
const [fileId] = item.id.split('/');
|
||||||
|
if (!lastOpenedFileIds[fileId]) {
|
||||||
|
// Remove item from the store
|
||||||
|
store.commit(`${type}/deleteItem`, item.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1 +1,24 @@
|
|||||||
export default {};
|
import store from '../../store';
|
||||||
|
import googleHelper from '../helpers/googleHelper';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
downloadContent(token, fileId) {
|
||||||
|
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
||||||
|
return googleHelper.downloadAppDataFile(token, syncData.id)
|
||||||
|
.then((content) => {
|
||||||
|
if (content.updated !== syncData.updated) {
|
||||||
|
store.dispatch('data/setSyncData', {
|
||||||
|
...store.getters['data/syncData'],
|
||||||
|
[syncData.id]: {
|
||||||
|
...syncData,
|
||||||
|
updated: content.updated,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
history: [],
|
||||||
|
...content,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -5,8 +5,6 @@ import utils from './utils';
|
|||||||
import userActivitySvc from './userActivitySvc';
|
import userActivitySvc from './userActivitySvc';
|
||||||
import gdriveAppDataProvider from './providers/gdriveAppDataProvider';
|
import gdriveAppDataProvider from './providers/gdriveAppDataProvider';
|
||||||
import googleHelper from './helpers/googleHelper';
|
import googleHelper from './helpers/googleHelper';
|
||||||
import emptyContent from '../data/emptyContent';
|
|
||||||
import emptySyncContent from '../data/emptySyncContent';
|
|
||||||
|
|
||||||
const lastSyncActivityKey = 'lastSyncActivity';
|
const lastSyncActivityKey = 'lastSyncActivity';
|
||||||
let lastSyncActivity;
|
let lastSyncActivity;
|
||||||
@ -36,6 +34,7 @@ function setLastSyncActivity() {
|
|||||||
|
|
||||||
function getSyncProvider(syncLocation) {
|
function getSyncProvider(syncLocation) {
|
||||||
switch (syncLocation.provider) {
|
switch (syncLocation.provider) {
|
||||||
|
case 'gdriveAppData':
|
||||||
default:
|
default:
|
||||||
return gdriveAppDataProvider;
|
return gdriveAppDataProvider;
|
||||||
}
|
}
|
||||||
@ -43,11 +42,21 @@ function getSyncProvider(syncLocation) {
|
|||||||
|
|
||||||
function getSyncToken(syncLocation) {
|
function getSyncToken(syncLocation) {
|
||||||
switch (syncLocation.provider) {
|
switch (syncLocation.provider) {
|
||||||
|
case 'gdriveAppData':
|
||||||
default:
|
default:
|
||||||
return store.getters['data/loginToken'];
|
return store.getters['data/loginToken'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)
|
||||||
|
// Item does not exist, create it
|
||||||
|
.catch(() => store.commit(`${type}/setItem`, {
|
||||||
|
id: `${fileId}/${type}`,
|
||||||
|
}));
|
||||||
|
const loadContent = loader('content');
|
||||||
|
const loadSyncContent = loader('syncContent');
|
||||||
|
const loadContentState = loader('contentState');
|
||||||
|
|
||||||
function applyChanges(changes) {
|
function applyChanges(changes) {
|
||||||
const storeItemMap = { ...store.getters.allItemMap };
|
const storeItemMap = { ...store.getters.allItemMap };
|
||||||
const syncData = { ...store.getters['data/syncData'] };
|
const syncData = { ...store.getters['data/syncData'] };
|
||||||
@ -164,23 +173,51 @@ function sync() {
|
|||||||
const existingSyncData = store.getters['data/syncDataByItemId'][contentId];
|
const existingSyncData = store.getters['data/syncDataByItemId'][contentId];
|
||||||
});
|
});
|
||||||
|
|
||||||
const syncOneContent = fileId => localDbSvc.retrieveItem(`${fileId}/syncContent`)
|
const syncOneContent = fileId => loadSyncContent(fileId)
|
||||||
.catch(() => ({ ...emptySyncContent(), id: `${fileId}/syncContent` }))
|
.then(() => loadContent(fileId))
|
||||||
.then(syncContent => localDbSvc.retrieveItem(`${fileId}/content`)
|
.then(() => {
|
||||||
.catch(() => ({ ...emptyContent(), id: `${fileId}/content` }))
|
const getContent = () => store.state.content.itemMap[`${fileId}/content`];
|
||||||
.then((content) => {
|
const getSyncContent = () => store.state.content.itemMap[`${fileId}/syncContent`];
|
||||||
const syncOneContentLocation = (syncLocation) => {
|
|
||||||
return Promise.resolve()
|
|
||||||
.then(() => {
|
|
||||||
const provider = getSyncProvider(syncLocation);
|
|
||||||
const token = getSyncToken(syncLocation);
|
|
||||||
return provider && token && provider.downloadContent()
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncLocations = [{ provider: null }, ...content.syncLocations];
|
const syncLocations = [
|
||||||
return syncOneContentLocation(syncLocations[0]);
|
{ id: 'main', provider: 'gdriveAppData' },
|
||||||
}));
|
...getContent().syncLocations.filter(syncLocation => getSyncToken(syncLocation),
|
||||||
|
)];
|
||||||
|
const downloadedLocations = {};
|
||||||
|
|
||||||
|
const syncOneContentLocation = () => {
|
||||||
|
let result;
|
||||||
|
syncLocations.some((syncLocation) => {
|
||||||
|
if (!downloadedLocations[syncLocation.id]) {
|
||||||
|
const provider = getSyncProvider(syncLocation);
|
||||||
|
const token = getSyncToken(syncLocation);
|
||||||
|
result = provider && token && provider.downloadContent(fileId, syncLocation)
|
||||||
|
.then((content) => {
|
||||||
|
const syncContent = getSyncContent();
|
||||||
|
const syncLocationData = syncContent.syncLocationData[syncLocation.id] || {
|
||||||
|
history: [],
|
||||||
|
};
|
||||||
|
let lastMergedContent;
|
||||||
|
syncLocationData.history.some((updated) => {
|
||||||
|
if (content.history.indexOf(updated) !== -1) {
|
||||||
|
|
||||||
|
}
|
||||||
|
return lastMergedContent;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => syncOneContentLocation());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
return syncOneContentLocation();
|
||||||
|
})
|
||||||
|
.then(() => localDbSvc.unloadContents(), (err) => {
|
||||||
|
localDbSvc.unloadContents();
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
// Called until no content to save
|
// Called until no content to save
|
||||||
const saveNextContent = ifNotTooLate(() => {
|
const saveNextContent = ifNotTooLate(() => {
|
||||||
@ -189,7 +226,7 @@ function sync() {
|
|||||||
const updated = getContentUpdated(contentId);
|
const updated = getContentUpdated(contentId);
|
||||||
const existingSyncData = store.getters['data/syncDataByItemId'][contentId];
|
const existingSyncData = store.getters['data/syncDataByItemId'][contentId];
|
||||||
if (!existingSyncData || existingSyncData.updated !== updated) {
|
if (!existingSyncData || existingSyncData.updated !== updated) {
|
||||||
saveContentPromise = localDbSvc.retrieveItem(contentId)
|
saveContentPromise = localDbSvc.loadItem(contentId)
|
||||||
.then(content => googleHelper.saveItem(
|
.then(content => googleHelper.saveItem(
|
||||||
googleToken,
|
googleToken,
|
||||||
// Use deepCopy to freeze objects
|
// Use deepCopy to freeze objects
|
||||||
@ -299,19 +336,11 @@ localDbSvc.sync()
|
|||||||
store.dispatch('data/setLastOpenedId', currentFile.id);
|
store.dispatch('data/setLastOpenedId', currentFile.id);
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
// Load contentState from DB
|
// Load contentState from DB
|
||||||
.then(() => localDbSvc.retrieveItem(`${currentFile.id}/contentState`)
|
.then(() => loadContentState(currentFile.id))
|
||||||
// contentState does not exist, create it
|
|
||||||
.catch(() => store.commit('contentState/setItem', {
|
|
||||||
id: `${currentFile.id}/contentState`,
|
|
||||||
})))
|
|
||||||
// Load syncContent from DB
|
// Load syncContent from DB
|
||||||
.then(() => localDbSvc.retrieveItem(`${currentFile.id}/syncContent`)
|
.then(() => loadSyncContent(currentFile.id))
|
||||||
// syncContent does not exist, create it
|
|
||||||
.catch(() => store.commit('syncContent/setItem', {
|
|
||||||
id: `${currentFile.id}/syncContent`,
|
|
||||||
})))
|
|
||||||
// Load content from DB
|
// Load content from DB
|
||||||
.then(() => localDbSvc.retrieveItem(`${currentFile.id}/content`));
|
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`));
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
@ -320,6 +349,14 @@ localDbSvc.sync()
|
|||||||
// Sync local DB periodically
|
// Sync local DB periodically
|
||||||
utils.setInterval(() => localDbSvc.sync(), 1000);
|
utils.setInterval(() => localDbSvc.sync(), 1000);
|
||||||
|
|
||||||
|
// Unload contents from memory periodically
|
||||||
|
utils.setInterval(() => {
|
||||||
|
// Wait for sync and publish to finish
|
||||||
|
if (store.state.queue.isEmpty) {
|
||||||
|
localDbSvc.unloadContents();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
isSyncAvailable,
|
isSyncAvailable,
|
||||||
requestSync,
|
requestSync,
|
||||||
|
@ -25,13 +25,13 @@ const patcher = id => ({ state, commit }, data) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const localSettingsToggler = propertyName => ({ getters, dispatch }, value) => dispatch('patchLocalSettings', {
|
|
||||||
[propertyName]: value === undefined ? !getters.localSettings[propertyName] : value,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Local settings
|
// Local settings
|
||||||
module.getters.localSettings = getter('localSettings');
|
module.getters.localSettings = getter('localSettings');
|
||||||
module.actions.patchLocalSettings = patcher('localSettings');
|
module.actions.patchLocalSettings = patcher('localSettings');
|
||||||
|
const localSettingsToggler = propertyName => ({ getters, dispatch }, value) => dispatch('patchLocalSettings', {
|
||||||
|
[propertyName]: value === undefined ? !getters.localSettings[propertyName] : value,
|
||||||
|
});
|
||||||
module.actions.toggleNavigationBar = localSettingsToggler('showNavigationBar');
|
module.actions.toggleNavigationBar = localSettingsToggler('showNavigationBar');
|
||||||
module.actions.toggleEditor = localSettingsToggler('showEditor');
|
module.actions.toggleEditor = localSettingsToggler('showEditor');
|
||||||
module.actions.toggleSidePreview = localSettingsToggler('showSidePreview');
|
module.actions.toggleSidePreview = localSettingsToggler('showSidePreview');
|
||||||
@ -48,7 +48,7 @@ module.getters.lastOpened = getter('lastOpened');
|
|||||||
const getLastOpenedIds = (lastOpened, rootState) => Object.keys(lastOpened)
|
const getLastOpenedIds = (lastOpened, rootState) => Object.keys(lastOpened)
|
||||||
.filter(id => rootState.file.itemMap[id])
|
.filter(id => rootState.file.itemMap[id])
|
||||||
.sort((id1, id2) => lastOpened[id2] - lastOpened[id1])
|
.sort((id1, id2) => lastOpened[id2] - lastOpened[id1])
|
||||||
.slice(0, 10);
|
.slice(0, 20);
|
||||||
module.getters.lastOpenedIds = (state, getters, rootState) =>
|
module.getters.lastOpenedIds = (state, getters, rootState) =>
|
||||||
getLastOpenedIds(getters.lastOpened, rootState);
|
getLastOpenedIds(getters.lastOpened, rootState);
|
||||||
module.actions.setLastOpenedId = ({ getters, commit, rootState }, fileId) => {
|
module.actions.setLastOpenedId = ({ getters, commit, rootState }, fileId) => {
|
||||||
@ -80,18 +80,18 @@ module.actions.setSyncData = setter('syncData');
|
|||||||
module.getters.tokens = getter('tokens');
|
module.getters.tokens = getter('tokens');
|
||||||
module.getters.googleTokens = (state, getters) => getters.tokens.google || {};
|
module.getters.googleTokens = (state, getters) => getters.tokens.google || {};
|
||||||
module.getters.loginToken = (state, getters) => {
|
module.getters.loginToken = (state, getters) => {
|
||||||
// Return the first googleToken that has the isLogin flag
|
// Return the first google token that has the isLogin flag
|
||||||
const googleTokens = getters.googleTokens;
|
const googleTokens = getters.googleTokens;
|
||||||
const loginSubs = Object.keys(googleTokens)
|
const loginSubs = Object.keys(googleTokens)
|
||||||
.filter(sub => googleTokens[sub].isLogin);
|
.filter(sub => googleTokens[sub].isLogin);
|
||||||
return googleTokens[loginSubs[0]];
|
return googleTokens[loginSubs[0]];
|
||||||
};
|
};
|
||||||
module.actions.patchTokens = patcher('tokens');
|
module.actions.patchTokens = patcher('tokens');
|
||||||
module.actions.setGoogleToken = ({ getters, dispatch }, googleToken) => {
|
module.actions.setGoogleToken = ({ getters, dispatch }, token) => {
|
||||||
dispatch('patchTokens', {
|
dispatch('patchTokens', {
|
||||||
google: {
|
google: {
|
||||||
...getters.googleTokens,
|
...getters.googleTokens,
|
||||||
[googleToken.sub]: googleToken,
|
[token.sub]: token,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user