@ -9,36 +9,60 @@ import { randomInteger } from "../random";
import { AppState } from "../types" ;
type Id = string ;
type Ids = Id [ ] ;
type ElementLike = {
id : string ;
version : number ;
versionNonce : number ;
parent? : string | null ;
} ;
type Cache = Record < string , ExcalidrawElement | undefined > ;
const parseId = ( uid : string ) = > {
const [ , parent , id , version ] = uid . match (
const createElement = ( opts : { uid : string } | ElementLike ) = > {
let uid : string ;
let id : string ;
let version : number | null ;
let parent : string | null = null ;
let versionNonce : number | null = null ;
if ( "uid" in opts ) {
const match = opts . uid . match (
/^(?:\((\^|\w+)\))?(\w+)(?::(\d+))?(?:\((\w+)\))?$/ ,
) ! ;
parent = match [ 1 ] ;
id = match [ 2 ] ;
version = match [ 3 ] ? parseInt ( match [ 3 ] ) : null ;
uid = version ? ` ${ id } : ${ version } ` : id ;
} else {
( { id , version , versionNonce } = opts ) ;
parent = parent || null ;
uid = id ;
}
return {
uid : version ? ` ${ id } : ${ version } ` : id ,
uid ,
id ,
version : version ? parseInt ( version ) : null ,
version ,
versionNonce : versionNonce || randomInteger ( ) ,
parent : parent || null ,
} ;
} ;
const idsToElements = (
ids : Ids ,
ids : ( Id | ElementLike ) [ ] ,
cache : Cache = { } ,
) : readonly ExcalidrawElement [ ] = > {
return ids . reduce ( ( acc , _uid , idx ) = > {
const { uid , id , version , parent } = parseId ( _uid ) ;
const { uid , id , version , parent , versionNonce } = createElement (
typeof _uid === "string" ? { uid : _uid } : _uid ,
) ;
const cached = cache [ uid ] ;
const elem = {
id ,
version : version ? ? 0 ,
versionNonce : randomInteger ( ) ,
versionNonce ,
. . . cached ,
parent ,
} as BroadcastedExcalidrawElement ;
// @ts-ignore
cache [ uid ] = elem ;
acc . push ( elem ) ;
return acc ;
@ -67,8 +91,8 @@ const cleanElements = (elements: ReconciledElements) => {
const cloneDeep = ( data : any ) = > JSON . parse ( JSON . stringify ( data ) ) ;
const test = < U extends ` $ { string } : $ { " L " | " R " } ` > (
local : Ids ,
remote : Ids ,
local : ( Id | ElementLike ) [ ] ,
remote : ( Id | ElementLike ) [ ] ,
target : U [ ] ,
bidirectional = true ,
) = > {
@ -80,6 +104,7 @@ const test = <U extends `${string}:${"L" | "R"}`>(
return ( source === "L" ? _local : _remote ) . find ( ( e ) = > e . id === id ) ! ;
} ) as any as ReconciledElements ;
const remoteReconciled = reconcileElements ( _local , _remote , { } as AppState ) ;
expect ( target . length ) . equal ( remoteReconciled . length ) ;
expect ( cleanElements ( remoteReconciled ) ) . deep . equal (
cleanElements ( _target ) ,
"remote reconciliation" ,
@ -301,4 +326,92 @@ describe("elements reconciliation", () => {
test ( [ "A:2" , "B:2" ] , [ "(A)C" , "B:1" ] , [ "A:L" , "C:R" , "B:L" ] ) ;
test ( [ "A:2" , "B:2" ] , [ "(A)C" , "B:1" ] , [ "A:L" , "C:R" , "B:L" ] ) ;
} ) ;
it ( "test identical elements reconciliation" , ( ) = > {
const testIdentical = (
local : ElementLike [ ] ,
remote : ElementLike [ ] ,
expected : Id [ ] ,
) = > {
const ret = reconcileElements (
local as any as ExcalidrawElement [ ] ,
remote as any as ExcalidrawElement [ ] ,
{ } as AppState ,
) ;
if ( new Set ( ret . map ( ( x ) = > x . id ) ) . size !== ret . length ) {
throw new Error ( "reconcileElements: duplicate elements found" ) ;
}
expect ( ret . map ( ( x ) = > x . id ) ) . to . deep . equal ( expected ) ;
} ;
// identical id/version/versionNonce
// -------------------------------------------------------------------------
testIdentical (
[ { id : "A" , version : 1 , versionNonce : 1 } ] ,
[ { id : "A" , version : 1 , versionNonce : 1 } ] ,
[ "A" ] ,
) ;
testIdentical (
[
{ id : "A" , version : 1 , versionNonce : 1 } ,
{ id : "B" , version : 1 , versionNonce : 1 } ,
] ,
[
{ id : "B" , version : 1 , versionNonce : 1 } ,
{ id : "A" , version : 1 , versionNonce : 1 } ,
] ,
[ "B" , "A" ] ,
) ;
testIdentical (
[
{ id : "A" , version : 1 , versionNonce : 1 } ,
{ id : "B" , version : 1 , versionNonce : 1 } ,
] ,
[
{ id : "B" , version : 1 , versionNonce : 1 } ,
{ id : "A" , version : 1 , versionNonce : 1 } ,
] ,
[ "B" , "A" ] ,
) ;
// actually identical (arrays and element objects)
// -------------------------------------------------------------------------
const elements1 = [
{
id : "A" ,
version : 1 ,
versionNonce : 1 ,
parent : null ,
} ,
{
id : "B" ,
version : 1 ,
versionNonce : 1 ,
parent : null ,
} ,
] ;
testIdentical ( elements1 , elements1 , [ "A" , "B" ] ) ;
testIdentical ( elements1 , elements1 . slice ( ) , [ "A" , "B" ] ) ;
testIdentical ( elements1 . slice ( ) , elements1 , [ "A" , "B" ] ) ;
testIdentical ( elements1 . slice ( ) , elements1 . slice ( ) , [ "A" , "B" ] ) ;
const el1 = {
id : "A" ,
version : 1 ,
versionNonce : 1 ,
parent : null ,
} ;
const el2 = {
id : "B" ,
version : 1 ,
versionNonce : 1 ,
parent : null ,
} ;
testIdentical ( [ el1 , el2 ] , [ el2 , el1 ] , [ "A" , "B" ] ) ;
} ) ;
} ) ;