import levelup , { LevelUp } from 'levelup' ; import sub from 'subleveldown' ; import { AbstractLevelDOWN , AbstractBatch } from 'abstract-leveldown' ; import { Ticket , RawGroup , GroupInfo , IKeyPair , ICrypto } from './types' ; import VirgilEncryptDown from './virgil-encrypt-down' ; export interface RetrieveOptions { ticketCount ? : number ; epochNumber ? : number ; } export interface GroupLocalStorageConstructorParams { identity : string ; leveldown : AbstractLevelDOWN ; virgilCrypto : ICrypto ; } export class GroupLocalStorage { private _db : LevelUp ; private _encryptionLevel : VirgilEncryptDown < string > ; constructor ( { identity , virgilCrypto , leveldown } ) { this . _encryptionLevel = new VirgilEncryptDown ( leveldown , { virgilCrypto } ) ; const rootLevel = levelup ( this . _encryptionLevel ) ; const identityLevel = sub ( rootLevel , identity ) ; this . _db = sub ( identityLevel , 'GROUPS' , { valueEncoding : 'json' } ) ; } async store ( rawGroup ) { const lastTicket = rawGroup . tickets [ rawGroup . tickets . length - 1 ] ; if ( ! lastTicket ) { throw new Error ( 'Attempted to store group without tickets.' ) ; } const { sessionId } = lastTicket . groupSessionMessage ; const insertInfo = { type : 'put' , key : sessionId , value : rawGroup . info , } ; const insertTickets = rawGroup . tickets . map ( ticket => ( { type : 'put' , key : this . getTicketKey ( sessionId , ticket . groupSessionMessage . epochNumber ) , value : ticket , } ) ) ; await this . _db . batch ( [ insertInfo , ... insertTickets ] ) ; } async retrieve ( sessionId , options ) { const hasTicketCount = typeof options . ticketCount === 'number' ; const hasEpochNumber = typeof options . epochNumber === 'number' ; if ( hasTicketCount === hasEpochNumber ) { throw new Error ( 'Either "ticketCount" or "epochNumber" option must be provided' ) ; } const [ info , tickets ] = await Promise . all < GroupInfo | null , Ticket [ ] > ( [ this . retrieveGroupInfo ( sessionId ) , options . ticketCount ? this . retrieveNLastTickets ( sessionId , options . ticketCount ) : this . retrieveTicketByEpochNumber ( sessionId , options . epochNumber ! ) , ] ) ; if ( ! info || tickets . length === 0 ) return null ; return { info , tickets } ; } async delete ( sessionId ) { const prefix = sessionId ; await this . _db . clear ( { gt : prefix , lte : prefix + '\xff' , } ) ; } async reset ( ) { await this . _db . clear ( ) ; } async addParticipants ( sessionId , participants ) { const [ ticket ] = await this . retrieveNLastTickets ( sessionId , 1 ) ; const newTicket = { participants : ticket . participants . concat ( participants ) , groupSessionMessage : ticket . groupSessionMessage , } ; const key = this . getTicketKey ( sessionId , ticket . groupSessionMessage . epochNumber ) ; await this . _db . batch ( [ { type : 'put' , key , value : newTicket } ] ) ; } setEncryptionKeyPair ( keyPair ) { this . _encryptionLevel . setKeyPair ( keyPair ) ; } private async retrieveGroupInfo ( sessionId ) { try { return await this . _db . get ( sessionId ) ; } catch ( err ) { if ( err . notFound ) { return null ; } throw err ; } } private retrieveNLastTickets ( sessionId , ticketCount ) { return new Promise ( ( resolve , reject ) => { const tickets = [ ] ; let error = undefined ; const prefix = sessionId + '!' ; this . _db . createReadStream ( { gt : prefix , lte : prefix + '\xff' , reverse : true , limit : ticketCount , } ) . on ( 'data' , data => tickets . unshift ( data . value ) ) . on ( 'error' , err => ( error = err ) ) . on ( 'end' , ( ) => ( error ? reject ( error ) : resolve ( tickets ) ) ) ; } ) ; } private async retrieveTicketByEpochNumber ( sessionId , epochNumber ) { const key = this . getTicketKey ( sessionId , epochNumber ) ; try { const ticket = await this . _db . get ( key ) ; return [ ticket ] ; } catch ( err ) { if ( err . notFound ) { return [ ] ; } throw err ; } } private getTicketKey ( sessionId , epochNumber ) { const epochNumberStr = String ( epochNumber ) ; const epochNumberEncodedLength = epochNumberStr . length . toString ( 36 ) ; return `${ sessionId }!${ epochNumberEncodedLength }!${ epochNumberStr }` ; } }