-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
277 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...nd/types/client.broadcast.payload.type.ts → ...w-backend/types/broadcast.payload.type.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
export enum ClientBroadcastPayloadType { | ||
export enum BroadcastPayloadType { | ||
SCENE_INIT = 'SCENE_INIT', | ||
SCENE_UPDATE = 'SCENE_UPDATE', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './idle.state'; | ||
export * from './server.broadcast.payload'; |
7 changes: 7 additions & 0 deletions
7
src/excalidraw-backend/types/events/server.broadcast.payload.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { ExcalidrawElement } from '../excalidraw.element'; | ||
import { ExcalidrawFile } from '../excalidraw.file'; | ||
|
||
export type ServerBroadcastPayload = { | ||
elements: readonly ExcalidrawElement[]; | ||
files: readonly ExcalidrawFile[]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export type ExcalidrawElement = { | ||
id: string; | ||
index: number; | ||
version: number; | ||
versionNonce: number; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export type ExcalidrawFile = Record<string, unknown>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
/** | ||
* Transforms array of objects containing `id` attribute, | ||
* or array of ids (strings), into a Map, keyd by `id`. | ||
*/ | ||
export const arrayToMap = <T extends { id: string } | string>( | ||
items: readonly T[] | Map<string, T>, | ||
) => { | ||
if (items instanceof Map) { | ||
return items; | ||
} | ||
return items.reduce((acc: Map<string, T>, element) => { | ||
acc.set(typeof element === 'string' ? element : element.id, element); | ||
return acc; | ||
}, new Map()); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { BasePayload, SocketEventData } from '../types'; | ||
|
||
/** | ||
* Tries to decode incoming binary data. | ||
* Throws an exception otherwise. | ||
* @param {ArrayBuffer}data The incoming binary data | ||
* @returns {SocketEventData} Returns the decoded data in form of event data | ||
* @throws {TypeError} Throws an error if the data cannot be decoded | ||
* @throws {SyntaxError} Thrown if the data to parse is not valid JSON. | ||
*/ | ||
export const tryDecodeIncoming = <TPayload extends BasePayload>( | ||
data: ArrayBuffer, | ||
): SocketEventData<TPayload> | never => { | ||
const strEventData = tryDecodeBinary(data); | ||
|
||
return JSON.parse(strEventData) as SocketEventData<TPayload>; | ||
}; | ||
/** | ||
* | ||
* @throws {TypeError} Throws an error if the data cannot be decoded | ||
*/ | ||
export const tryDecodeBinary = (data: ArrayBuffer): string => { | ||
const decoder = new TextDecoder('utf-8'); | ||
return decoder.decode(data); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
interface Item { | ||
id: string; | ||
[key: string]: any; | ||
} | ||
|
||
function arraysEqual(arr1: any[], arr2: any[]): boolean { | ||
if (arr1.length !== arr2.length) return false; | ||
for (let i = 0; i < arr1.length; i++) { | ||
if (arr1[i] !== arr2[i]) return false; | ||
} | ||
return true; | ||
} | ||
|
||
export function detectChanges( | ||
oldArray: readonly Item[], | ||
newArray: readonly Item[], | ||
) { | ||
const changes = { | ||
added: [] as Item[], | ||
removed: [] as Item[], | ||
updated: [] as { | ||
id: string; | ||
changes: { before: Partial<Item>; after: Partial<Item> }; | ||
}[], | ||
}; | ||
|
||
const oldMap = new Map<string, Item>(); | ||
const newMap = new Map<string, Item>(); | ||
|
||
oldArray.forEach((item) => oldMap.set(item.id, item)); | ||
newArray.forEach((item) => newMap.set(item.id, item)); | ||
|
||
// Detect added and updated items | ||
newMap.forEach((newItem, id) => { | ||
const oldItem = oldMap.get(id); | ||
if (!oldItem) { | ||
changes.added.push(newItem); | ||
} else { | ||
const before: Partial<Item> = {}; | ||
const after: Partial<Item> = {}; | ||
for (const key in newItem) { | ||
if (Array.isArray(newItem[key]) && Array.isArray(oldItem[key])) { | ||
if (!arraysEqual(newItem[key], oldItem[key])) { | ||
before[key] = oldItem[key]; | ||
after[key] = newItem[key]; | ||
} | ||
} else if (newItem[key] !== oldItem[key]) { | ||
before[key] = oldItem[key]; | ||
after[key] = newItem[key]; | ||
} | ||
} | ||
if (Object.keys(before).length > 0) { | ||
changes.updated.push({ id, changes: { before, after } }); | ||
} | ||
} | ||
}); | ||
|
||
// Detect removed items | ||
oldMap.forEach((oldItem, id) => { | ||
if (!newMap.has(id)) { | ||
changes.removed.push(oldItem); | ||
} | ||
}); | ||
|
||
return changes; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { ExcalidrawElement } from '../types'; | ||
import { arrayToMap } from './array.to.map'; | ||
|
||
const shouldDiscardRemoteElement = ( | ||
local: ExcalidrawElement | undefined, | ||
remote: ExcalidrawElement, | ||
): boolean => { | ||
return !!( | ||
local && | ||
// local element is newer | ||
(local.version > remote.version || | ||
// resolve conflicting edits deterministically by taking the one with | ||
// the lowest versionNonce | ||
(local.version === remote.version && | ||
local.versionNonce < remote.versionNonce)) | ||
); | ||
}; | ||
|
||
/*const validateIndicesThrottled = throttle( | ||
( | ||
orderedElements: readonly OrderedExcalidrawElement[], | ||
localElements: readonly OrderedExcalidrawElement[], | ||
remoteElements: readonly RemoteExcalidrawElement[], | ||
) => { | ||
if ( | ||
import.meta.env.DEV || | ||
import.meta.env.MODE === ENV.TEST || | ||
window?.DEBUG_FRACTIONAL_INDICES | ||
) { | ||
// create new instances due to the mutation | ||
const elements = syncInvalidIndices( | ||
orderedElements.map((x) => ({ ...x })), | ||
); | ||
validateFractionalIndices(elements, { | ||
// throw in dev & test only, to remain functional on `DEBUG_FRACTIONAL_INDICES` | ||
shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST, | ||
includeBoundTextValidation: true, | ||
reconciliationContext: { | ||
localElements, | ||
remoteElements, | ||
}, | ||
}); | ||
} | ||
}, | ||
1000 * 60, | ||
{ leading: true, trailing: false }, | ||
);*/ | ||
|
||
export const reconcileElements = ( | ||
localElements: readonly ExcalidrawElement[], | ||
remoteElements: readonly ExcalidrawElement[], | ||
): ExcalidrawElement[] => { | ||
const localElementsMap = arrayToMap(localElements); | ||
const reconciledElements: ExcalidrawElement[] = []; | ||
const added = new Set<string>(); | ||
|
||
// process remote elements | ||
for (const remoteElement of remoteElements) { | ||
if (!added.has(remoteElement.id)) { | ||
const localElement = localElementsMap.get(remoteElement.id); | ||
const discardRemoteElement = shouldDiscardRemoteElement( | ||
localElement, | ||
remoteElement, | ||
); | ||
|
||
if (localElement && discardRemoteElement) { | ||
reconciledElements.push(localElement); | ||
added.add(localElement.id); | ||
} else { | ||
reconciledElements.push(remoteElement); | ||
added.add(remoteElement.id); | ||
} | ||
} | ||
} | ||
|
||
// process remaining local elements | ||
for (const localElement of localElements) { | ||
if (!added.has(localElement.id)) { | ||
reconciledElements.push(localElement); | ||
added.add(localElement.id); | ||
} | ||
} | ||
|
||
// const orderedElements = orderByFractionalIndex(reconciledElements); | ||
|
||
// validateIndicesThrottled(orderedElements, localElements, remoteElements); | ||
|
||
// de-duplicate indices | ||
// syncInvalidIndices(orderedElements); | ||
|
||
// return orderedElements as ReconciledExcalidrawElement[]; | ||
return reconciledElements; | ||
}; |