import ServiceElectronIpc , { IPC } from '../../../../../../services/service.electron.ipc' ; import { Observable , Subject , Subscription } from 'rxjs' ; import { ControllerSessionTabStreamOutput } from '../../../output/controller.session.tab.stream.output' ; import { IBookmark } from '../../../bookmarks/controller.session.tab.stream.bookmarks' ; import { ControllerSessionTabTimestamp } from '../../../timestamps/session.dependency.timestamps' ; import { ControllerSessionScope } from '../../../scope/controller.session.tab.scope' ; import { extractPluginId , extractRowPosition , clearRowStr , } from '../../../../../helpers/row.helpers' ; import { EKey , EParent , ISelectionAccessor , } from '../../../../../../services/standalone/service.output.redirections' ; import { IRow } from '../../../row/controller.row.api' ; import { Dependency , SessionGetter , SearchSessionGetter } from '../search.dependency' ; import { IHotkeyEvent } from '../../../../../../services/service.hotkeys' ; import OutputRedirectionsService from '../../../../../../services/standalone/service.output.redirections' ; import HotkeysService from '../../../../../../services/service.hotkeys' ; import * as Toolkit from 'chipmunk.client.toolkit' ; export type TRequestDataHandler = ( start : number , end : number ) => Promise < IPC . StreamChunk > ; export interface IParameters { guid : string ; requestDataHandler : TRequestDataHandler ; stream : ControllerSessionTabStreamOutput ; timestamp : ControllerSessionTabTimestamp ; scope : ControllerSessionScope ; } export interface IStreamState { originalCount : number ; count : number ; bookmarksCount : number ; stored : IRange ; frame : IRange ; lastLoadingRequestId : any ; bufferLoadingRequestId : any ; } export interface IStreamStateEvent { isBookmarkInjection : boolean ; state : IStreamState ; } export interface IRange { start : number ; end : number ; } export interface ILoadedRange { range : IRange ; rows : IRow [ ] ; } export const Settings = { trigger : 1000 , maxRequestCount : 2000 , maxStoredCount : 2000 , requestDelay : 0 , } ; export class ControllerSessionTabSearchOutput implements Dependency { private _uuid : string ; private _logger : Toolkit . Logger ; private _rows : IRow [ ] = [ ] ; private _subscriptions : { [ key ] : Toolkit . Subscription | Subscription ; } = { } ; private _preloadTimestamp : number = - 1 ; private _lastRequestedRows : IRow [ ] = [ ] ; private _state : IStreamState = { count : 0 , originalCount : 0 , bookmarksCount : 0 , stored : { start : - 1 , end : - 1 , } , frame : { start : - 1 , end : - 1 , } , lastLoadingRequestId : undefined , bufferLoadingRequestId : undefined , } ; private _subjects = { onStateUpdated : new Subject < IStreamStateEvent > ( ) , onReset : new Subject < void > ( ) , onRangeLoaded : new Subject < ILoadedRange > ( ) , onBookmarksChanged : new Subject < void > ( ) , onScrollTo : new Subject < number > ( ) , } ; private _accessor : { session : SessionGetter ; search : SearchSessionGetter ; } ; constructor ( uuid , session , search ) { this . _uuid = uuid ; this . _accessor = { session , search , } ; this . _logger = new Toolkit . Logger ( `ControllerSessionTabSearchOutput: ${ this . _uuid }` ) ; } public init ( ) { return new Promise ( ( resolve , reject ) => { this . _subscriptions . onRowSelected = OutputRedirectionsService . subscribe ( this . _uuid , this . _onRowSelected . bind ( this ) ) ; this . _subscriptions . onAddedBookmark = this . _accessor . session ( ) . getBookmarks ( ) . getObservable ( ) . onAdded . subscribe ( this . _onAddedBookmarksState . bind ( this ) ) ; this . _subscriptions . onRemovedBookmark = this . _accessor . session ( ) . getBookmarks ( ) . getObservable ( ) . onRemoved . subscribe ( this . _onRemovedBookmarksState . bind ( this ) ) ; this . _subscriptions . selectAllSearchResult = HotkeysService . getObservable ( ) . selectAllSearchResult . subscribe ( this . _onSelectAllSearchResult . bind ( this ) ) ; resolve ( ) ; } ) ; } public destroy ( ) { return new Promise ( ( resolve ) => { Object . keys ( this . _subscriptions ) . forEach ( ( key ) => { this . _subscriptions [ key ] . unsubscribe ( ) ; } ) ; resolve ( ) ; } ) ; } public getName ( ) { return 'ControllerSessionTabSearchOutput' ; } returns { onStateUpdated public getObservable ( ) { return { onStateUpdated : this . _subjects . onStateUpdated . asObservable ( ) , onRangeLoaded : this . _subjects . onRangeLoaded . asObservable ( ) , onBookmarksChanged : this . _subjects . onBookmarksChanged . asObservable ( ) , onReset : this . _subjects . onReset . asObservable ( ) , onScrollTo : this . _subjects . onScrollTo . asObservable ( ) , } ; } public getFrame ( ) { return { start : this . _state . frame . start >= 0 ? this . _state . frame . start : 0 , end : this . _state . frame . end >= 0 ? this . _state . frame . end : 0 , } ; } public getRange ( range ) { let rows = [ ] ; if ( isNaN ( range . start ) || isNaN ( range . end ) || ! isFinite ( range . start ) || ! isFinite ( range . end ) ) { return new Error ( `Range has incorrect format. Start and end shound be finite and not NaN` ) ; } if ( range . start < 0 || range . end < 0 ) { return [ ] ; } if ( this . _state . count === 0 ) { return [ ] ; } const offset = this . _state . stored . start < 0 ? 0 : this . _state . stored . start ; const indexes = { start : range . start - offset , end : range . end - offset , } ; const stored = Object . assign ( { } , this . _state . stored ) ; if ( indexes . start > this . _rows . length - 1 ) { rows = this . _getPendingPackets ( range . start , range . end + 1 ) ; } else if ( indexes . start < 0 && indexes . end < 0 ) { rows = this . _getPendingPackets ( range . start , range . end + 1 ) ; } else if ( indexes . start >= 0 && indexes . start <= this . _rows . length - 1 && indexes . end <= this . _rows . length - 1 ) { rows = this . _rows . slice ( indexes . start , indexes . end + 1 ) ; } else if ( indexes . start >= 0 && indexes . start <= this . _rows . length - 1 && indexes . end > this . _rows . length - 1 ) { rows = this . _rows . slice ( indexes . start , this . _rows . length ) ; rows . push ( ... this . _getPendingPackets ( this . _rows . length , indexes . end + 1 ) ) ; } else if ( indexes . start < 0 && indexes . end >= 0 && indexes . end <= this . _rows . length - 1 ) { rows = this . _rows . slice ( 0 , indexes . end + 1 ) ; rows . unshift ( ... this . _getPendingPackets ( 0 , Math . abs ( indexes . start ) ) ) ; } if ( stored . start !== this . _state . stored . start || stored . end !== this . _state . stored . end ) { return new Error ( this . _logger . warn ( `State was changed. Was { start: ${ stored . start }, end: ${ stored . end }}, became { start: ${ this . _state . stored . start }, end: ${ this . _state . stored . end }}.` ) ) ; } if ( rows . length !== range . end - range . start + 1 ) { return new Error ( this . _logger . error ( `Calculation error: gotten ${ rows . length } rows; should be: ${ range . end - range . start + 1 }. State was { start: ${ stored . start }, end: ${ stored . end }}, became { start: ${ this . _state . stored . start }, end: ${ this . _state . stored . end }}.` ) ) ; } this . setFrame ( range ) ; return rows ; } public loadRange ( range ) { return new Promise ( ( resolve , reject ) => { if ( isNaN ( range . start ) || isNaN ( range . end ) || ! isFinite ( range . start ) || ! isFinite ( range . end ) ) { return reject ( new Error ( `Range has incorrect format. Start and end shound be finite and not NaN` ) ) ; } this . _requestData ( range . start , range . end ) . then ( ( message ) => { if ( message . data === undefined ) { return resolve ( [ ] ) ; } const packets = [ ] ; this . _parse ( message . data , message . start , message . end , packets ) ; resolve ( packets . filter ( ( packet ) => { return packet . position >= range . start && packet . position <= range . end ; } ) ) ; } ) . catch ( ( error ) => { reject ( new Error ( `Error during requesting data (rows from ${ range . start } to ${ range . end }): ${ error . message }` ) ) ; } ) ; } ) ; } public setFrame ( range ) { if ( this . _state . originalCount === 0 || this . _state . count === 0 ) { return ; } range . end = range . end > this . _state . originalCount - 1 ? this . _state . originalCount - 1 : range . end ; this . _state . frame = Object . assign ( { } , range ) ; if ( ! this . _load ( ) ) { } } returns public getState ( ) { return Object . assign ( { } , this . _state ) ; } returns public clearStream ( ) { this . _rows = [ ] ; this . _state = { count : 0 , originalCount : 0 , bookmarksCount : 0 , stored : { start : - 1 , end : - 1 , } , frame : { start : - 1 , end : - 1 , } , lastLoadingRequestId : undefined , bufferLoadingRequestId : undefined , } ; this . _subjects . onReset . next ( ) ; } param { number } rows returns { IRange | undefined } public updateStreamState ( rowsCount ) { if ( rowsCount === 0 ) { this . clearStream ( ) ; } this . _setTotalStreamCount ( rowsCount ) ; this . _updateBookmarksData ( ) ; this . _subjects . onStateUpdated . next ( { isBookmarkInjection : false , state : Object . assign ( { } , this . _state ) , } ) ; } public getRowsCount ( ) { return this . _state . count ; } public preload ( range ) { range . end = range . end > this . _state . originalCount - 1 ? this . _state . originalCount - 1 : range . end ; return this . _preload ( range ) ; } public getRowByPosition ( position ) { return this . _rows . find ( ( row ) => { return row . positionInStream === position ; } ) ; } public scrollTo ( row ) { this . _subjects . onScrollTo . next ( row ) ; } private _isRangeStored ( range ) { const stored = Object . assign ( { } , this . _state . stored ) ; if ( this . _state . count === 0 || range . start < 0 || range . end < 0 ) { return true ; } if ( range . start >= stored . start && range . end <= stored . end ) { return true ; } return false ; } private _getRowsSliced ( from , to ) { const offset = this . _state . stored . start > 0 ? this . _state . stored . start : 0 ; return this . _rows . slice ( from - offset , to - offset ) ; } private _onRowSelected ( sender , selection , clicked ) { if ( sender === EParent . search ) { return ; } this . _subjects . onScrollTo . next ( clicked ) ; } private _onAddedBookmarksState ( bookmark ) { this . _updateBookmarksData ( bookmark ) ; } private _onRemovedBookmarksState ( index ) { this . _updateBookmarksData ( undefined ) ; } private _updateBookmarksData ( bookmark ? ) { this . _rows = this . _removeBookmarks ( this . _rows ) ; this . _rows = this . _insertBookmarks ( this . _rows ) ; this . _state . stored . start = this . _getFirstPosNotBookmarkedRow ( ) . position ; this . _state . stored . end = this . _getLastPosNotBookmarkedRow ( ) . position ; const count = this . _state . count ; this . _setTotalStreamCount ( this . _state . originalCount ) ; if ( this . _state . count !== count ) { this . _subjects . onStateUpdated . next ( { isBookmarkInjection : true , state : Object . assign ( { } , this . _state ) , } ) ; } if ( bookmark !== undefined ) { this . _subjects . onBookmarksChanged . next ( ) ; } } private _insertBookmarks ( rows ) { const bookmarks = this . _accessor . session ( ) . getBookmarks ( ) . get ( ) ; const indexes = Array . from ( bookmarks . keys ( ) ) ; const add = ( target , from , to ) => { const between = this . _getBetween ( indexes , from , to ) ; between . forEach ( ( index ) => { if ( index === from || index === to ) { return ; } const inserted = bookmarks . get ( index ) ; if ( inserted === undefined ) { this . _logger . warn ( `Fail to add bookmark with index ${ index }. Bookmark isn't found` ) ; return ; } target . push ( { str : inserted . str , positionInStream : inserted . position , position : - 1 , pluginId : inserted . pluginId , sessionId : this . _uuid , parent : EParent . search , api : this . _accessor . session ( ) . getRowAPI ( ) , } ) ; this . _state . bookmarksCount += 1 ; } ) ; } ; this . _state . bookmarksCount = 0 ; if ( indexes . length === 0 ) { return rows ; } indexes . sort ( ( a , b ) => a - b ) ; const updated = [ ] ; if ( rows . length === 0 ) { add ( updated , - 1 , Infinity ) ; return updated ; } rows . forEach ( ( row , i ) => { if ( i === rows . length - 1 && row . position !== this . _state . originalCount - 1 ) { updated . push ( row ) ; return ; } else if ( i === rows . length - 1 && row . position === this . _state . originalCount - 1 ) { if ( i === 0 && row . position === 0 ) { add ( updated , - 1 , row . positionInStream ) ; } updated . push ( row ) ; add ( updated , row . positionInStream , Infinity ) ; return ; } else if ( i === 0 && row . position === 0 ) { add ( updated , - 1 , row . positionInStream ) ; } updated . push ( row ) ; add ( updated , row . positionInStream , rows [ i + 1 ] . positionInStream ) ; } ) ; return updated ; } private _removeBookmarks ( rows ) { this . _state . bookmarksCount = 0 ; return rows . filter ( ( row ) => { return row . position !== - 1 ; } ) ; } private _getFirstPosNotBookmarkedRow ( ) { for ( let i = 0 , max = this . _rows . length - 1 ; i <= max ; i += 1 ) { if ( this . _rows [ i ] . position !== - 1 ) { return { position : this . _rows [ i ] . position , index : i } ; } } return { position : - 1 , index : - 1 } ; } private _getLastPosNotBookmarkedRow ( ) { for ( let i = this . _rows . length - 1 ; i >= 0 ; i -= 1 ) { if ( this . _rows [ i ] . position !== - 1 ) { return { position : this . _rows [ i ] . position , index : i } ; } } return { position : - 1 , index : - 1 } ; } private _getBetween ( indexes , from , to ) { return indexes . filter ( ( value ) => { return value >= from ? ( value <= to ? true : false ) : false ; } ) ; } private _load ( ) { clearTimeout ( this . _state . lastLoadingRequestId ) ; const frame = Object . assign ( { } , this . _state . frame ) ; const stored = this . _state . stored ; if ( stored . start <= frame . start && stored . end >= frame . end ) { return false ; } const request = { start : - 1 , end : - 1 } ; if ( frame . end - frame . start + 1 <= 0 ) { return false ; } const distance = { toStart : frame . start , toEnd : this . _state . originalCount - 1 - frame . end , } ; if ( distance . toStart > Settings . maxRequestCount / 2 ) { request . start = distance . toStart - Settings . maxRequestCount / 2 ; } else { request . start = 0 ; } if ( distance . toEnd > Settings . maxRequestCount / 2 ) { request . end = frame . end + Settings . maxRequestCount / 2 ; } else { request . end = this . _state . originalCount - 1 ; } this . _state . lastLoadingRequestId = setTimeout ( ( ) => { this . _state . lastLoadingRequestId = undefined ; this . _requestData ( request . start , request . end ) . then ( ( message ) => { if ( message . data === undefined ) { return ; } if ( this . _state . lastLoadingRequestId !== undefined ) { this . _lastRequestedRows = [ ] ; this . _parse ( message . data , message . start , message . end , this . _lastRequestedRows , frame ) ; return ; } this . _setTotalStreamCount ( message . rows ) ; this . _parse ( message . data , message . start , message . end ) ; if ( stored . start <= frame . start && stored . end >= frame . end ) { this . _updateBookmarksData ( ) ; this . _subjects . onRangeLoaded . next ( { range : { start : frame . start , end : frame . end } , rows : this . _getRowsSliced ( frame . start , frame . end + 1 ) , } ) ; return ; } else { this . _logger . warn ( `Requested frame isn't in scope of stored data. Request - rows from ${ request . start } to ${ request . end }.` ) ; } } ) . catch ( ( error ) => { this . _logger . error ( `Error during requesting data (rows from ${ request . start } to ${ request . end }): ${ error . message }` ) ; } ) ; } , Settings . requestDelay ) ; return true ; } private _preload ( range ) { return new Promise ( ( resolve , reject ) => { const timestamp = Date . now ( ) ; if ( this . _preloadTimestamp !== - 1 && timestamp - this . _preloadTimestamp < 500 ) { return resolve ( null ) ; } if ( this . _isRangeStored ( range ) ) { this . _preloadTimestamp = - 1 ; return resolve ( range ) ; } this . _preloadTimestamp = timestamp ; this . _requestData ( range . start , range . end ) . then ( ( message ) => { this . _preloadTimestamp = - 1 ; this . _setTotalStreamCount ( message . rows ) ; this . _parse ( message . data , message . start , message . end ) ; this . _updateBookmarksData ( ) ; resolve ( { start : message . start , end : message . end } ) ; } ) . catch ( ( error ) => { this . _preloadTimestamp = - 1 ; reject ( new Error ( this . _logger . error ( `Fail to preload data (rows from ${ range . start } to ${ range . end }) due error: ${ error . message }` ) ) ) ; } ) ; } ) ; } param { string } input param { number } from param { number } to param { number } count returns private _parse ( input , from , to , dest ? , frame ? ) { const rows = input . split ( /\n/gi ) ; let packets = [ ] ; try { rows . forEach ( ( str , i ) => { if ( frame !== undefined ) { if ( frame . end < from + i ) { throw new Error ( 'No need to parse' ) ; } if ( frame . start > from + i ) { return ; } } const position = extractRowPosition ( str ) ; const pluginId = extractPluginId ( str ) ; packets . push ( { str : clearRowStr ( str ) , position : from + i , positionInStream : position , pluginId : pluginId , sessionId : this . _uuid , parent : EParent . search , api : this . _accessor . session ( ) . getRowAPI ( ) , } ) ; } ) ; } catch ( e ) { } packets = packets . filter ( ( packet ) => { return packet . positionInStream !== - 1 ; } ) ; if ( dest !== undefined ) { dest . push ( ... packets ) ; } else { if ( packets . length !== to - from + 1 ) { throw new Error ( `Count of gotten rows dismatch with defined range. Range: ${ from }-${ to }. Actual count: ${ rows . length }; expected count: ${ to - from }.` ) ; } this . _acceptPackets ( packets ) ; } } private _getPendingPackets ( first , last ) { const rows = Array . from ( { length : last - first } ) . map ( ( _ , i ) => { return { position : first + i , positionInStream : first + i , str : this . _lastRequestedRows [ i ] === undefined ? undefined : this . _lastRequestedRows [ i ] . str , pluginId : this . _lastRequestedRows [ i ] === undefined ? - 1 : this . _lastRequestedRows [ i ] . pluginId , rank : this . _accessor . session ( ) . getStreamOutput ( ) . getRank ( ) , sessionId : this . _uuid , parent : EParent . search , api : this . _accessor . session ( ) . getRowAPI ( ) , } ; } ) ; return rows ; } param { IRow [ ] } packets param { number } start param { number } end returns private _acceptPackets ( packets ) { if ( packets . length === 0 ) { return ; } const packet = { start : packets [ 0 ] . position , end : packets [ packets . length - 1 ] . position , } ; if ( this . _rows . length === 0 ) { this . _rows . push ( ... packets ) ; } else if ( packet . start > this . _state . stored . end ) { this . _rows = packets ; } else if ( packet . end < this . _state . stored . start ) { this . _rows = packets ; } else if ( packet . start < this . _state . stored . start ) { this . _rows = this . _removeBookmarks ( this . _rows ) ; const range = this . _state . stored . start - packet . start ; const injection = packets . slice ( 0 , range ) ; this . _rows . unshift ( ... injection ) ; if ( this . _rows . length > Settings . maxStoredCount ) { const toCrop = this . _rows . length - Settings . maxStoredCount ; this . _rows . splice ( - toCrop , toCrop ) ; } } else if ( packet . end > this . _state . stored . end ) { this . _rows = this . _removeBookmarks ( this . _rows ) ; const range = packet . end - this . _state . stored . end ; const injection = packets . slice ( packets . length - range , packets . length ) ; this . _rows . push ( ... injection ) ; if ( this . _rows . length > Settings . maxStoredCount ) { const toCrop = this . _rows . length - Settings . maxStoredCount ; this . _rows . splice ( 0 , toCrop ) ; } } this . _rows = this . _insertBookmarks ( this . _rows ) ; this . _state . stored . start = this . _getFirstPosNotBookmarkedRow ( ) . position ; this . _state . stored . end = this . _getLastPosNotBookmarkedRow ( ) . position ; } private _setTotalStreamCount ( count ) { this . _state . originalCount = count ; this . _state . count = this . _state . originalCount + this . _state . bookmarksCount ; if ( this . _state . count === 0 ) { return this . clearStream ( ) ; } } private _requestData ( start , end ) { return new Promise ( ( resolve , reject ) => { const s = Date . now ( ) ; ServiceElectronIpc . request < IPC . SearchChunk > ( new IPC . SearchChunk ( { guid : this . _uuid , start : start , end : end , } ) , IPC . SearchChunk ) . then ( ( response ) => { this . _logger . env ( `Chunk [${ start } - ${ end }] is read in: ${ ( ( Date . now ( ) - s ) / 1000 ) . toFixed ( 2 ) }s` ) ; if ( response . error !== undefined ) { return reject ( new Error ( this . _logger . warn ( `Request to stream chunk was finished within error: ${ response . error }` ) ) ) ; } if ( response . guid === undefined || response . data === undefined || response . length === undefined || response . rows === undefined ) { return reject ( new Error ( this . _logger . warn ( `SearchChunk returns invalid data ${ JSON . stringify ( response ) }` ) ) ) ; } resolve ( { guid : response . guid , data : response . data , start : response . start , end : response . end , length : response . length , rows : response . rows , } ) ; } ) ; } ) ; } private _onSelectAllSearchResult ( event ) { const active = this . _accessor . session ( ) . getGuid ( ) ; if ( active !== event . session ) { return ; } const bookmarks = Array . from ( this . _accessor . session ( ) . getBookmarks ( ) . get ( ) . values ( ) ) ; bookmarks . sort ( ( a , b ) => ( a . position > b . position ? 1 : - 1 ) ) ; const count = this . _state . originalCount ; if ( count === 0 ) { if ( bookmarks . length > 0 ) { OutputRedirectionsService . clear ( active ) ; bookmarks . forEach ( ( bookmark ) => { OutputRedirectionsService . select ( EParent . search , active , { output : bookmark . position , search : - 1 } , undefined , EKey . ctrl ) ; } ) ; } return ; } const coors = { begin : undefined , end : undefined , } ; OutputRedirectionsService . clear ( active ) ; Promise . all ( [ this . loadRange ( { start : 0 , end : 0 } ) . then ( ( rows ) => { if ( rows . length !== 1 ) { return ; } coors . begin = rows [ 0 ] ; } ) , this . loadRange ( { start : count - 1 , end : count - 1 } ) . then ( ( rows ) => { if ( rows . length !== 1 ) { return ; } coors . end = rows [ 0 ] ; } ) , ] ) . then ( ( ) => { if ( coors . begin === undefined || coors . end === undefined ) { this . _logger . warn ( `Some ranges weren't found` ) ; return ; } if ( bookmarks . length !== 0 ) { const first = bookmarks [ 0 ] ; const last = bookmarks [ bookmarks . length - 1 ] ; if ( first . position < coors . begin . positionInStream ) { coors . begin . positionInStream = first . position ; coors . begin . position = - 1 ; } if ( last . position < coors . begin . positionInStream ) { coors . begin . positionInStream = last . position ; coors . begin . position = - 1 ; } } OutputRedirectionsService . select ( EParent . search , active , { output : coors . begin . positionInStream , search : coors . begin . position } , undefined , EKey . ignore ) ; OutputRedirectionsService . select ( EParent . search , active , { output : coors . end . positionInStream , search : coors . end . position } , undefined , EKey . shift ) ; } ) . catch ( ( err ) => { this . _logger . warn ( `Fail request ranges due error: ${ err . message }` ) ; } ) ; } }