fix(tldraw): only send diffs updates to server

When a shape is changed, the full shape objcect was being transmitted to the server again.
Do a diff to only send what changed (similarly  as it was in tldraw v1) to save upload bw.

TODO:
Draw segments diffs (array) is still not working, so all the segments are still being sent every time.
This commit is contained in:
germanocaumo 2024-09-19 12:38:57 -03:00
parent eef565ec81
commit f7468b6fc2
4 changed files with 64 additions and 18 deletions

View File

@ -50,6 +50,7 @@ class WhiteboardModel extends SystemConfiguration {
val wb = getWhiteboard(wbId) val wb = getWhiteboard(wbId)
var annotationsAdded = Array[AnnotationVO]() var annotationsAdded = Array[AnnotationVO]()
var annotationsDiffAdded = Array[AnnotationVO]()
var newAnnotationsMap = wb.annotationsMap var newAnnotationsMap = wb.annotationsMap
for (annotation <- annotations) { for (annotation <- annotations) {
@ -57,21 +58,10 @@ class WhiteboardModel extends SystemConfiguration {
if (oldAnnotation.isDefined) { if (oldAnnotation.isDefined) {
val hasPermission = isPresenter || isModerator || oldAnnotation.get.userId == userId val hasPermission = isPresenter || isModerator || oldAnnotation.get.userId == userId
if (hasPermission) { if (hasPermission) {
// Determine if the annotation is a line shape val mergedAnnotationInfo = deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo)
val isLineShape = annotation.annotationInfo.get("type").contains("line")
// Merge old and new annotation properties with special handling for line shape
val mergedAnnotationInfo = if (isLineShape) {
val newProps = annotation.annotationInfo.get("props").asInstanceOf[Option[Map[String, Any]]].getOrElse(Map.empty)
val oldProps = oldAnnotation.get.annotationInfo.get("props").asInstanceOf[Option[Map[String, Any]]].getOrElse(Map.empty)
val updatedProps = overwriteLineShapeHandles(oldProps, newProps)
annotation.annotationInfo + ("props" -> updatedProps)
} else {
deepMerge(oldAnnotation.get.annotationInfo, annotation.annotationInfo)
}
// Apply cleaning if it's an arrow annotation // Apply cleaning if it's an arrow annotation
val finalAnnotationInfo = if (annotation.annotationInfo.get("type").contains("arrow")) { val finalAnnotationInfo = if (oldAnnotation.get.annotationInfo.get("type").contains("arrow")) {
cleanArrowAnnotationProps(mergedAnnotationInfo) cleanArrowAnnotationProps(mergedAnnotationInfo)
} else { } else {
mergedAnnotationInfo mergedAnnotationInfo
@ -80,6 +70,7 @@ class WhiteboardModel extends SystemConfiguration {
val newAnnotation = oldAnnotation.get.copy(annotationInfo = finalAnnotationInfo) val newAnnotation = oldAnnotation.get.copy(annotationInfo = finalAnnotationInfo)
newAnnotationsMap += (annotation.id -> newAnnotation) newAnnotationsMap += (annotation.id -> newAnnotation)
annotationsAdded :+= newAnnotation annotationsAdded :+= newAnnotation
annotationsDiffAdded :+= annotation
println(s"Updated annotation on page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].") println(s"Updated annotation on page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
} else { } else {
println(s"User $userId doesn't have permission to edit annotation ${annotation.id}, ignoring...") println(s"User $userId doesn't have permission to edit annotation ${annotation.id}, ignoring...")
@ -87,6 +78,7 @@ class WhiteboardModel extends SystemConfiguration {
} else if (annotation.annotationInfo.contains("type")) { } else if (annotation.annotationInfo.contains("type")) {
newAnnotationsMap += (annotation.id -> annotation) newAnnotationsMap += (annotation.id -> annotation)
annotationsAdded :+= annotation annotationsAdded :+= annotation
annotationsDiffAdded :+= annotation
println(s"Adding annotation to page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].") println(s"Adding annotation to page [${wb.id}]. After numAnnotations=[${newAnnotationsMap.size}].")
} else { } else {
println(s"New annotation [${annotation.id}] with no type, ignoring...") println(s"New annotation [${annotation.id}] with no type, ignoring...")
@ -97,7 +89,7 @@ class WhiteboardModel extends SystemConfiguration {
val newWb = wb.copy(annotationsMap = newAnnotationsMap) val newWb = wb.copy(annotationsMap = newAnnotationsMap)
saveWhiteboard(newWb) saveWhiteboard(newWb)
annotationsAdded annotationsDiffAdded
} }
private def overwriteLineShapeHandles(oldProps: Map[String, Any], newProps: Map[String, Any]): Map[String, Any] = { private def overwriteLineShapeHandles(oldProps: Map[String, Any], newProps: Map[String, Any]): Map[String, Any] = {

View File

@ -25,6 +25,7 @@ import {
mapLanguage, mapLanguage,
isValidShapeType, isValidShapeType,
usePrevious, usePrevious,
getDifferences,
} from './utils'; } from './utils';
import { useMouseEvents, useCursor } from './hooks'; import { useMouseEvents, useCursor } from './hooks';
import { notifyShapeNumberExceeded, getCustomEditorAssetUrls, getCustomAssetUrls } from './service'; import { notifyShapeNumberExceeded, getCustomEditorAssetUrls, getCustomAssetUrls } from './service';
@ -507,7 +508,15 @@ const Whiteboard = React.memo((props) => {
}, },
}; };
shapeBatchRef.current[updatedRecord.id] = updatedRecord; const diff = getDifferences(prevShapesRef.current[record?.id], updatedRecord);
if (diff) {
diff.id = record.id;
shapeBatchRef.current[updatedRecord.id] = diff;
} else {
shapeBatchRef.current[updatedRecord.id] = updatedRecord;
}
}); });
// Handle removed shapes immediately (not batched) // Handle removed shapes immediately (not batched)

View File

@ -30,11 +30,40 @@ const mapLanguage = (language) => {
} }
}; };
const getDifferences = (prev, next) => {
// Check if the two values are the same
if (prev === next) return undefined;
// If both are arrays, find the differences
if (Array.isArray(prev) && Array.isArray(next)) {
const differences = next.filter((item, index) => prev[index] !== item);
return differences.length > 0 ? differences : [];
}
// If both are objects, recursively check their properties
if (typeof prev === 'object' && typeof next === 'object' && prev !== null && next !== null) {
const differences = {};
Object.keys(next).forEach((key) => {
if (Object.prototype.hasOwnProperty.call(next, key)) {
const diff = getDifferences(prev[key], next[key]);
if (diff !== undefined) {
differences[key] = diff;
}
}
});
return Object.keys(differences).length > 0 ? differences : undefined;
}
// For other types, return the next value if different
return next;
};
const Utils = { const Utils = {
usePrevious, mapLanguage, isValidShapeType, usePrevious, mapLanguage, isValidShapeType, getDifferences,
}; };
export default Utils; export default Utils;
export { export {
usePrevious, mapLanguage, isValidShapeType, usePrevious, mapLanguage, isValidShapeType, getDifferences,
}; };

View File

@ -575,6 +575,17 @@ def determine_slide_number(slide, current_slide)
slide slide
end end
def clean_arrow_end_or_start_props(props)
if props['type'] == 'binding'
# Remove 'x' and 'y' for 'binding' type
props = props.except('x', 'y')
elsif props['type'] == 'point'
# Remove unwanted properties for 'point' type
props = props.except('boundShapeId', 'normalizedAnchor', 'isExact', 'isPrecise')
end
props
end
def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide, timestamp) def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide, timestamp)
presentation = event.at_xpath('presentation') presentation = event.at_xpath('presentation')
slide = event.at_xpath('pageNumber') slide = event.at_xpath('pageNumber')
@ -615,6 +626,11 @@ def events_parse_tldraw_shape(shapes, event, current_presentation, current_slide
prev_shape[:out] = timestamp prev_shape[:out] = timestamp
shape[:shape_unique_id] = prev_shape[:shape_unique_id] shape[:shape_unique_id] = prev_shape[:shape_unique_id]
shape[:shape_data] = prev_shape[:shape_data].deep_merge(shape[:shape_data]) shape[:shape_data] = prev_shape[:shape_data].deep_merge(shape[:shape_data])
# special handling to remove unwanted merged arrow props in tldraw v2
if shape[:shape_data]['type'] == 'arrow' && shape[:shape_data].key?('props')
shape[:shape_data]['props']['start'] = clean_arrow_end_or_start_props(shape[:shape_data]['props']['start'])
shape[:shape_data]['props']['end'] = clean_arrow_end_or_start_props(shape[:shape_data]['props']['end'])
end
else else
shape[:shape_unique_id] = @svg_shape_unique_id shape[:shape_unique_id] = @svg_shape_unique_id
@svg_shape_unique_id += 1 @svg_shape_unique_id += 1