Skip to content

Commit 1eb485a

Browse files
authored
Element sorting handling inserted templates (#33)
* sorting * prevent loops while sorting * version bump
1 parent d083623 commit 1eb485a

File tree

4 files changed

+39
-15
lines changed

4 files changed

+39
-15
lines changed

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "whiteboard-collaboration-service",
3-
"version": "0.4.1",
3+
"version": "0.4.2",
44
"description": "Alkemio Whiteboard Collaboration Service for Excalidraw backend",
55
"author": "Alkemio Foundation",
66
"private": false,

src/excalidraw-backend/server.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -505,8 +505,11 @@ export class Server {
505505
): ThrottledSaveFunction {
506506
const throttledSave = throttle(
507507
async (roomId: string) => {
508-
await this.saveRoom(roomId);
509-
this.notifyRoomSaved(roomId);
508+
const hasSaved = await this.saveRoom(roomId);
509+
510+
if (hasSaved) {
511+
this.notifyRoomSaved(roomId);
512+
}
510513
},
511514
wait,
512515
{ leading: true, trailing: true },
@@ -572,21 +575,23 @@ export class Server {
572575
return snapshotContent;
573576
}
574577

575-
private async saveRoom(roomId: string) {
578+
private async saveRoom(roomId: string): Promise<boolean> {
576579
const snapshot = this.snapshots.get(roomId);
577580
if (!snapshot) {
578581
this.logger.error(
579582
`No snapshot found for room '${roomId}' in the local storage!`,
580583
);
581-
return;
584+
return false;
582585
}
583586

584587
const cleanContent = prepareContentForSave(snapshot);
585588
const { data } = await this.utilService.save(roomId, cleanContent);
586589
if (isSaveErrorData(data)) {
587590
this.logger.error(`Failed to save room '${roomId}': ${data.error}`);
591+
return false;
588592
} else {
589593
this.logger.verbose?.(`Room '${roomId}' saved successfully`);
594+
return true;
590595
}
591596
}
592597
}

src/excalidraw-backend/utils/reconcile.ts

+27-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { Logger } from '@nestjs/common';
2+
import { unionBy } from 'lodash';
13
import { arrayToMap } from './array.to.map';
24
import { ExcalidrawElement } from '../../excalidraw/types';
35
import { arrayToMapBy } from './array.to.map.by';
4-
import { Logger } from '@nestjs/common';
56
// import { orderByFractionalIndex, syncInvalidIndices } from './fractionalIndex';
67

78
const shouldDiscardRemoteElement = (
@@ -97,20 +98,28 @@ export const reconcileElements = (
9798
// de-duplicate indices
9899
// const syncedElemented = syncInvalidIndices(orderedElements);
99100
try {
100-
return orderByPrecedingElement(reconciledElements);
101+
return tryOrderByPrecedingElement(reconciledElements);
101102
} catch (error) {
102103
logger.warn(`Element sorting failed with error: '${error.message}'`);
103104
return reconciledElements;
104105
}
105106
};
106107

107-
const orderByPrecedingElement = (
108+
/**
109+
* Will try to order elements by preceding element.
110+
* Order is not guaranteed, if there are multiple "first" elements, or a preceding element that does not exist.
111+
* Returns a half sorted array if there is an element with preceding element that does not exist.
112+
* @param unOrderedElements
113+
* @throws Error if there is more than one element with preceding element = '^'
114+
*/
115+
const tryOrderByPrecedingElement = (
108116
unOrderedElements: ExcalidrawElement[],
109117
): ExcalidrawElement[] | never => {
110118
// for zero or one element return the same array, as it's already sorted
111119
if (unOrderedElements.length < 2) {
112120
return unOrderedElements;
113121
}
122+
// const elementsWithPreceding = unOrderedElements.filter(el.
114123
// validated there is just one starting element
115124
const startElements = unOrderedElements.filter(
116125
(el) => el.__precedingElement__ === '^',
@@ -131,14 +140,24 @@ const orderByPrecedingElement = (
131140
// the array is starting with element that has no preceding element
132141
let parentElement = startElements[0];
133142
orderedElements.push(parentElement);
134-
// Follow the chain of __precedingElement__
135-
while (true) {
143+
// keep track of visited elements to prevent cycles
144+
const visitedElements = new Set<string>();
145+
// Follow the chain of __precedingElement__ until we have sorted all
146+
while (orderedElements.length != unOrderedElements.length) {
147+
// prevent cycles in the chain
148+
if (visitedElements.has(parentElement.id)) {
149+
throw new Error('Cycle detected in __precedingElement__ chain');
150+
}
151+
visitedElements.add(parentElement.id);
152+
// a child of a parent, is an element which __precedingElement__ is pointing to the parent
153+
// is there an element which preceding element is the parent element
136154
const childElement = elementMapByPrecedingKey.get(parentElement.id);
137-
155+
// there is a parent, which has no child; the chain is broken
138156
if (!childElement) {
139-
// we have reached the end of the chain
140-
break;
157+
// switch both arrays; combine ordered and switch the unordered at the end
158+
return unionBy(orderedElements, unOrderedElements, 'id');
141159
}
160+
// the final element is pointing to the one before it (preceding element)
142161

143162
orderedElements.push(childElement);
144163
parentElement = childElement;

0 commit comments

Comments
 (0)