<commit_before>import Immutable from "immutable";
import utils from "../common/utils"
import Apis from "../rpc_api/ApiInstances.js"
import {object_type} from "../chain/chain_types";
import validation from "common/validation"

let op_history   = parseInt(object_type.operation_history, 10);
let limit_order  = parseInt(object_type.limit_order, 10);
let balance_type  = parseInt(object_type.balance, 10);
let vesting_balance_type  = parseInt(object_type.vesting_balance, 10);
let witness_object_type  = parseInt(object_type.witness, 10);
let worker_object_type  = parseInt(object_type.worker, 10);
let committee_member_object_type  = parseInt(object_type.committee_member, 10);
let account_object_type  = parseInt(object_type.account, 10);
let asset_object_type  = parseInt(object_type.asset, 10);

let order_prefix = "1." + limit_order + "."
let balance_prefix = "1." + balance_type + "."
let vesting_balance_prefix = "1." + vesting_balance_type + "."
let witness_prefix = "1." + witness_object_type + "."
let worker_prefix = "1." + worker_object_type + "."
let committee_prefix = "1." + committee_member_object_type + "."
let asset_prefix = "1." + asset_object_type + "."
let account_prefix = "1." + account_object_type + "."

const DEBUG = false

/**
 *  @brief maintains a local cache of blockchain state 
 *  
 *  The ChainStore maintains a local cache of blockchain state and exposes
 *  an API that makes it easy to query objects and receive updates when
 *  objects are available. 
 */
class ChainStore 
{
   constructor() {
      /** tracks everyone who wants to receive updates when the cache changes */
      this.subscribers = new Set()
      this.clearCache()
   }

   /**
    * Clears all cached state.  This should be called any time the network connection is
    * reset.
    */
   clearCache() {
      this.objects_by_id            = Immutable.Map()
      this.accounts_by_name         = Immutable.Map()
      this.assets_by_id             = Immutable.Map()
      this.assets_by_symbol         = Immutable.Map()
      this.account_ids_by_key       = Immutable.Map()
      this.get_account_refs_of_keys_calls = Immutable.Map()
      this.account_history_requests = new Map() ///< tracks pending history requests
      this.subscriptions_by_market  = new Map()
      this.witness_by_account_id    = new Map()
      this.committee_by_account_id  = new Map()
      this.objects_by_vote_id       = new Map()
      this.subscribed = undefined

   }

   init() {
      return Apis.instance().db_api().exec( "set_subscribe_callback", [ this.onUpdate.bind(this), true ] )
          .then( v => { this.subscribed = true })
          .catch( error => {
            console.log( "Error: ", error )
           } )
   }

   onUpdate( updated_objects ) /// map from account id to objects
   {
      if(DEBUG) console.log( "updated objects: ", updated_objects )
      for( let a = 0; a < updated_objects.length; ++a )
      {
         for( let i = 0; i < updated_objects[a].length; ++i )
         {
            let obj = updated_objects[a][i]

            if( utils.is_object_id( obj ) ) /// the object was removed
               this.objects_by_id = this.objects_by_id.set( obj, null )
            else
               this._updateObject( obj, false )
         }
      }
      this.notifySubscribers()
   }

   notifySubscribers()
   {
      this.subscribers.forEach( (callback) => { callback() } )
   }

   /**
    *  Add a callback that will be called anytime any object in the cache is updated
    */
   subscribe( callback ) {
      this.subscribers.add( callback )
   }

   /**
    *  Remove a callback that was previously added via subscribe
    */
   unsubscribe( callback ) {
      this.subscribers.delete( callback ) 
   }

   /** Clear an object from the cache to force it to be fetched again. This may
    * be useful if a query failed the first time and the wallet has reason to believe
    * it may succeede the second time.
    */
   clearObjectCache( id ) {
      this.objects_by_id = this.objects_by_id.delete(id)
   }

   /**
    * There are three states an object id could be in:
    *
    * 1. undefined       - returned if a query is pending
    * 3. defined         - return an object
    * 4. null            - query return null
    *
    */
   getObject( id )
   {
      if( !utils.is_object_id(id) ) 
         throw Error( "argument is not an object id: " + id )

      let result = this.objects_by_id.get( id )

      if( result === undefined )
         return this.fetchObject( id )
      if( result === true ) 
         return undefined

      return result
   }

   /**
    *  @return undefined if a query is pending
    *  @return null if id_or_symbol has been queired and does not exist
    *  @return object if the id_or_symbol exists 
    */
   getAsset( id_or_symbol ) 
   {
      if( !id_or_symbol ) 
         return null

      if( utils.is_object_id( id_or_symbol ) )
         return getObject( id_or_symbol )

      /// TODO: verify id_or_symbol is a valid symbol name

      let asset_id = this.assets_by_symbol.get( id_or_symbol )

      if( utils.is_object_id( asset_id ) )
         return getObject( asset_id )

      if( asset_id === null ) 
         return null

      if( asset_id === true ) 
         return undefined

      Apis.instance().db_api().exec( "lookup_asset_symbols", [ [id_or_symbol] ] )
          .then( asset_objects => {
              if( asset_objects.length && asset_objects[0] )
                 this._updateObject( asset_objects[0], true )
              else
                 this.assets_by_symbol = this.assets_by_symbol.set( id_or_symbol, null )
      }).catch( error => {
         console.log( "Error: ", error )
         this.assets_by_symbol = this.assets_by_symbol.delete( id_or_symbol )
      } )

      return undefined
   }

   /**
    *  @param the public key to find accounts that reference it
    *
    *  @return Set of account ids that reference the given key
    *  @return null if no accounts reference the given key
    *  @return undefined if the result is unknown
    *
    *  If this method returns undefined, then it will send a request to
    *  the server for the current set of accounts after which the
    *  server will notify us of any accounts that reference these keys
    */
   getAccountRefsOfKey( key )
   {
      if( this.get_account_refs_of_keys_calls.contains(key) )
         return this.account_ids_by_key.get( key )
      else
      {
         this.get_account_refs_of_keys_calls = this.get_account_refs_of_keys_calls.add(key)
         Apis.instance().db_api().exec( "get_key_references", [ [key] ] )
         .then( vec_account_id => {
                  let refs = Immutable.Set()
                  refs = refs.withMutations( r => {
                     for( let i = 0; i < vec_account_id.length; ++i ) {
                        r.add(vec_account_id[i]) 
                     }
                  } )
                  this.account_ids_by_key = this.account_ids_by_key.set( key, refs )
                },
                error => {
                  this.account_ids_by_key             = this.account_ids_by_key.delete( key )
                  this.get_account_refs_of_keys_calls = this.get_account_refs_of_keys_calls.delete(key)
                })
         return undefined
      }
      return undefined
   }

   /**
    * @return a Set of balance ids that are claimable with the given address
    * @return undefined if a query is pending and the set is not known at this time
    * @return a empty Set if no items are found
    *
    * If this method returns undefined, then it will send a request to the server for
    * the current state after which it will be subscribed to changes to this set.
    */
   getBalanceObjects( address )
   {
      let current = this.balance_objects_by_address.get( address )
      if( current === undefined )
      {
          /** because balance objects are simply part of the genesis state, there is no need to worry about
           * having to update them / merge them or index them in updateObject.
           */
          this.balance_objects_by_address = this.balance_objects_by_address.set( address, Immutable.Set() )
          Apis.instance().db_api().exec( "get_balance_objects", [ [address] ] )
              .then( balance_objects => {
                         let set = new Set()
                         for( let i = 0; i < balance_objects.length; ++i )
                         {
                            this._updateObject( balance_objects[i] )
                            set.add(balance_objects[i].id)
                         }
                         this.balance_objects_by_address = this.balance_objects_by_address.set( address, Immutable.Set(set) )
                     },
                     error => {
                         this.balance_objects_by_address = this.balance_objects_by_address.delete( address )
                     } )
      }
      return this.balance_objects_by_address.get( address )
   }

   
   /**
    *  If there is not already a pending request to fetch this object, a new
    *  request will be made.
    *
    *  @return null if the object does not exist,
    *  @return undefined if the object might exist but is not in cache
    *  @return the object if it does exist and is in our cache
    */
   fetchObject( id )
   {
      if( typeof id !== 'string' )
      {
         let result = []
         for( let i = 0; i < id.length; ++i )
            result.push( this.fetchObject( id[i] ) )
         return result
      }

      if(DEBUG) console.log( "fetchObject: ", id )
      if( this.subscribed  !== true ) 
         return undefined

      if(DEBUG) console.log( "maybe fetch object: ", id )
      if( !utils.is_object_id(id) ) 
         throw Error( "argument is not an object id: " + id )

      if( id.substring( 0, 4 ) == "1.2." )
         return this.fetchFullAccount( id )
      
      let result = this.objects_by_id.get( id )
      if( result === undefined ) { // the fetch
         if(DEBUG) console.log( "fetching object: ", id )
         this.objects_by_id = this.objects_by_id.set( id, true )
         Apis.instance().db_api().exec( "get_objects", [ [id] ] ).then( optional_objects => {
                    //if(DEBUG) console.log('... optional_objects',optional_objects ? optional_objects[0].id : null)
                   for(let i = 0; i < optional_objects.length; i++) {
                       let optional_object = optional_objects[i]
                       if( optional_object )
                          this._updateObject( optional_object, true ) 
                       else 
                          this.objects_by_id = this.objects_by_id.set( id, null )
                   }
               }).catch( error => { // in the event of an error clear the pending state for id
                   console.log('!!! Chain API error',error)
                   this.objects_by_id = this.objects_by_id.delete(id)
               })
      }
      else if( result === true ) // then we are waiting a response
         return undefined
      return result // we have a response, return it
   }

   /**
    *  @return null if no such account exists
    *  @return undefined if such an account may exist, and fetch the the full account if not already pending
    *  @return the account object if it does exist
    */
   getAccount( name_or_id ) {
      if( utils.is_object_id(name_or_id) )
      {
         let account = getObject( name_or_id )
         if( account === undefined ) 
            return this.fetchFullAccount( name_or_id )
         return account
      }
      else if( validation.is_account_name( name_or_id ) )
      {
         let account_id = this.accounts_by_name.get( name_or_id )
         if( account_id === true ) // then a query is pending
            return undefined 
         if( account_id === undefined ) // then no query, fetch it
            return this.fetchFullAccount( name_or_id )
         return this.getObject( account_id ) // return it
      }
      throw Error( "Argument is not an account name or id: ", id  )
   }

   /**
    * This method will attempt to lookup the account, and then query to see whether or not there is
    * a witness for this account.  If the answer is known, it will return the witness_object, otherwise
    * it will attempt to look it up and return null.   Once the lookup has completed on_update will 
    * be called.
    *
    * @param id_or_account may either be an account_id, a witness_id, or an account_name
    */
   getWitness( id_or_account )
   {
      let account = getAccount( id_or_account )
      if( !account ) return null
      let account_id = account.get('id') 

      let witness_id = this.witness_by_account_id.get( account_id )
      if( witness_id === undefined )
         this.fetchWitnessByAccount( account_id )
      return getObject( witness_id )
       
      if( validation.is_account_name(id_or_account) || (id_or_account.substring(0,4) == "1.2."))<commit_msg>Add missing 'this.' for getObject method<commit_after>import Immutable from "immutable";
import utils from "../common/utils"
import Apis from "../rpc_api/ApiInstances.js"
import {object_type} from "../chain/chain_types";
import validation from "common/validation"

let op_history   = parseInt(object_type.operation_history, 10);
let limit_order  = parseInt(object_type.limit_order, 10);
let balance_type  = parseInt(object_type.balance, 10);
let vesting_balance_type  = parseInt(object_type.vesting_balance, 10);
let witness_object_type  = parseInt(object_type.witness, 10);
let worker_object_type  = parseInt(object_type.worker, 10);
let committee_member_object_type  = parseInt(object_type.committee_member, 10);
let account_object_type  = parseInt(object_type.account, 10);
let asset_object_type  = parseInt(object_type.asset, 10);

let order_prefix = "1." + limit_order + "."
let balance_prefix = "1." + balance_type + "."
let vesting_balance_prefix = "1." + vesting_balance_type + "."
let witness_prefix = "1." + witness_object_type + "."
let worker_prefix = "1." + worker_object_type + "."
let committee_prefix = "1." + committee_member_object_type + "."
let asset_prefix = "1." + asset_object_type + "."
let account_prefix = "1." + account_object_type + "."

const DEBUG = false

/**
 *  @brief maintains a local cache of blockchain state 
 *  
 *  The ChainStore maintains a local cache of blockchain state and exposes
 *  an API that makes it easy to query objects and receive updates when
 *  objects are available. 
 */
class ChainStore 
{
   constructor() {
      /** tracks everyone who wants to receive updates when the cache changes */
      this.subscribers = new Set()
      this.clearCache()
   }

   /**
    * Clears all cached state.  This should be called any time the network connection is
    * reset.
    */
   clearCache() {
      this.objects_by_id            = Immutable.Map()
      this.accounts_by_name         = Immutable.Map()
      this.assets_by_id             = Immutable.Map()
      this.assets_by_symbol         = Immutable.Map()
      this.account_ids_by_key       = Immutable.Map()
      this.get_account_refs_of_keys_calls = Immutable.Map()
      this.account_history_requests = new Map() ///< tracks pending history requests
      this.subscriptions_by_market  = new Map()
      this.witness_by_account_id    = new Map()
      this.committee_by_account_id  = new Map()
      this.objects_by_vote_id       = new Map()
      this.subscribed = undefined

   }

   init() {
      return Apis.instance().db_api().exec( "set_subscribe_callback", [ this.onUpdate.bind(this), true ] )
          .then( v => { this.subscribed = true })
          .catch( error => {
            console.log( "Error: ", error )
           } )
   }

   onUpdate( updated_objects ) /// map from account id to objects
   {
      if(DEBUG) console.log( "updated objects: ", updated_objects )
      for( let a = 0; a < updated_objects.length; ++a )
      {
         for( let i = 0; i < updated_objects[a].length; ++i )
         {
            let obj = updated_objects[a][i]

            if( utils.is_object_id( obj ) ) /// the object was removed
               this.objects_by_id = this.objects_by_id.set( obj, null )
            else
               this._updateObject( obj, false )
         }
      }
      this.notifySubscribers()
   }

   notifySubscribers()
   {
      this.subscribers.forEach( (callback) => { callback() } )
   }

   /**
    *  Add a callback that will be called anytime any object in the cache is updated
    */
   subscribe( callback ) {
      this.subscribers.add( callback )
   }

   /**
    *  Remove a callback that was previously added via subscribe
    */
   unsubscribe( callback ) {
      this.subscribers.delete( callback ) 
   }

   /** Clear an object from the cache to force it to be fetched again. This may
    * be useful if a query failed the first time and the wallet has reason to believe
    * it may succeede the second time.
    */
   clearObjectCache( id ) {
      this.objects_by_id = this.objects_by_id.delete(id)
   }

   /**
    * There are three states an object id could be in:
    *
    * 1. undefined       - returned if a query is pending
    * 3. defined         - return an object
    * 4. null            - query return null
    *
    */
   getObject( id )
   {
      if( !utils.is_object_id(id) ) 
         throw Error( "argument is not an object id: " + id )

      let result = this.objects_by_id.get( id )

      if( result === undefined )
         return this.fetchObject( id )
      if( result === true ) 
         return undefined

      return result
   }

   /**
    *  @return undefined if a query is pending
    *  @return null if id_or_symbol has been queired and does not exist
    *  @return object if the id_or_symbol exists 
    */
   getAsset( id_or_symbol ) 
   {
      if( !id_or_symbol ) 
         return null

      if( utils.is_object_id( id_or_symbol ) )
         return this.getObject( id_or_symbol )

      /// TODO: verify id_or_symbol is a valid symbol name

      let asset_id = this.assets_by_symbol.get( id_or_symbol )

      if( utils.is_object_id( asset_id ) )
         return this.getObject( asset_id )

      if( asset_id === null ) 
         return null

      if( asset_id === true ) 
         return undefined

      Apis.instance().db_api().exec( "lookup_asset_symbols", [ [id_or_symbol] ] )
          .then( asset_objects => {
              if( asset_objects.length && asset_objects[0] )
                 this._updateObject( asset_objects[0], true )
              else
                 this.assets_by_symbol = this.assets_by_symbol.set( id_or_symbol, null )
      }).catch( error => {
         console.log( "Error: ", error )
         this.assets_by_symbol = this.assets_by_symbol.delete( id_or_symbol )
      } )

      return undefined
   }

   /**
    *  @param the public key to find accounts that reference it
    *
    *  @return Set of account ids that reference the given key
    *  @return null if no accounts reference the given key
    *  @return undefined if the result is unknown
    *
    *  If this method returns undefined, then it will send a request to
    *  the server for the current set of accounts after which the
    *  server will notify us of any accounts that reference these keys
    */
   getAccountRefsOfKey( key )
   {
      if( this.get_account_refs_of_keys_calls.contains(key) )
         return this.account_ids_by_key.get( key )
      else
      {
         this.get_account_refs_of_keys_calls = this.get_account_refs_of_keys_calls.add(key)
         Apis.instance().db_api().exec( "get_key_references", [ [key] ] )
         .then( vec_account_id => {
                  let refs = Immutable.Set()
                  refs = refs.withMutations( r => {
                     for( let i = 0; i < vec_account_id.length; ++i ) {
                        r.add(vec_account_id[i]) 
                     }
                  } )
                  this.account_ids_by_key = this.account_ids_by_key.set( key, refs )
                },
                error => {
                  this.account_ids_by_key             = this.account_ids_by_key.delete( key )
                  this.get_account_refs_of_keys_calls = this.get_account_refs_of_keys_calls.delete(key)
                })
         return undefined
      }
      return undefined
   }

   /**
    * @return a Set of balance ids that are claimable with the given address
    * @return undefined if a query is pending and the set is not known at this time
    * @return a empty Set if no items are found
    *
    * If this method returns undefined, then it will send a request to the server for
    * the current state after which it will be subscribed to changes to this set.
    */
   getBalanceObjects( address )
   {
      let current = this.balance_objects_by_address.get( address )
      if( current === undefined )
      {
          /** because balance objects are simply part of the genesis state, there is no need to worry about
           * having to update them / merge them or index them in updateObject.
           */
          this.balance_objects_by_address = this.balance_objects_by_address.set( address, Immutable.Set() )
          Apis.instance().db_api().exec( "get_balance_objects", [ [address] ] )
              .then( balance_objects => {
                         let set = new Set()
                         for( let i = 0; i < balance_objects.length; ++i )
                         {
                            this._updateObject( balance_objects[i] )
                            set.add(balance_objects[i].id)
                         }
                         this.balance_objects_by_address = this.balance_objects_by_address.set( address, Immutable.Set(set) )
                     },
                     error => {
                         this.balance_objects_by_address = this.balance_objects_by_address.delete( address )
                     } )
      }
      return this.balance_objects_by_address.get( address )
   }

   
   /**
    *  If there is not already a pending request to fetch this object, a new
    *  request will be made.
    *
    *  @return null if the object does not exist,
    *  @return undefined if the object might exist but is not in cache
    *  @return the object if it does exist and is in our cache
    */
   fetchObject( id )
   {
      if( typeof id !== 'string' )
      {
         let result = []
         for( let i = 0; i < id.length; ++i )
            result.push( this.fetchObject( id[i] ) )
         return result
      }

      if(DEBUG) console.log( "fetchObject: ", id )
      if( this.subscribed  !== true ) 
         return undefined

      if(DEBUG) console.log( "maybe fetch object: ", id )
      if( !utils.is_object_id(id) ) 
         throw Error( "argument is not an object id: " + id )

      if( id.substring( 0, 4 ) == "1.2." )
         return this.fetchFullAccount( id )
      
      let result = this.objects_by_id.get( id )
      if( result === undefined ) { // the fetch
         if(DEBUG) console.log( "fetching object: ", id )
         this.objects_by_id = this.objects_by_id.set( id, true )
         Apis.instance().db_api().exec( "get_objects", [ [id] ] ).then( optional_objects => {
                    //if(DEBUG) console.log('... optional_objects',optional_objects ? optional_objects[0].id : null)
                   for(let i = 0; i < optional_objects.length; i++) {
                       let optional_object = optional_objects[i]
                       if( optional_object )
                          this._updateObject( optional_object, true ) 
                       else 
                          this.objects_by_id = this.objects_by_id.set( id, null )
                   }
               }).catch( error => { // in the event of an error clear the pending state for id
                   console.log('!!! Chain API error',error)
                   this.objects_by_id = this.objects_by_id.delete(id)
               })
      }
      else if( result === true ) // then we are waiting a response
         return undefined
      return result // we have a response, return it
   }

   /**
    *  @return null if no such account exists
    *  @return undefined if such an account may exist, and fetch the the full account if not already pending
    *  @return the account object if it does exist
    */
   getAccount( name_or_id ) {
      if( utils.is_object_id(name_or_id) )
      {
         let account = this.getObject( name_or_id )
         if( account === undefined ) 
            return this.fetchFullAccount( name_or_id )
         return account
      }
      else if( validation.is_account_name( name_or_id ) )
      {
         let account_id = this.accounts_by_name.get( name_or_id )
         if( account_id === true ) // then a query is pending
            return undefined 
         if( account_id === undefined ) // then no query, fetch it
            return this.fetchFullAccount( name_or_id )
         return this.getObject( account_id ) // return it
      }
      throw Error( "Argument is not an account name or id: ", id  )
   }

   /**
    * This method will attempt to lookup the account, and then query to see whether or not there is
    * a witness for this account.  If the answer is known, it will return the witness_object, otherwise
    * it will attempt to look it up and return null.   Once the lookup has completed on_update will 
    * be called.
    *
    * @param id_or_account may either be an account_id, a witness_id, or an account_name
    */
   getWitness( id_or_account )
   {
      let account = getAccount( id_or_account )
      if( !account ) return null
      let account_id = account.get('id') 

      let witness_id = this.witness_by_account_id.get( account_id )
      if( witness_id === undefined )
         this.fetchWitnessByAccount( account_id )
      return this.getObject( witness_id )
       
      if( validation.is_account_name(id_or_account) || (id_or_account.substring(0,4) == "1.2."))
{'fraction_non_alphanumeric': 0.09384265855443029, 'fraction_numerical': 0.0035426855096461164, 'mean_word_length': 2.0759208812033636, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2598126', 'n_tokens_mistral': 7657, 'n_tokens_neox': 7430, 'n_words': 2222}