import { Engine , Event , EventHandlers , EventMap , EventMaps , ExternalEngineState , RuleMap } from './rules_engine/Engine' ; import { ISODateTimeString , ISODateTimeStrings } from 'polar-shared/src/metadata/ISODateTimeStrings' ; import { Rule , RuleFactPair } from './rules_engine/Rule' ; import { Duration , DurationStr , TimeDurations } from 'polar-shared/src/util/TimeDurations' ; import { LifecycleEvents } from '../../../../web/js/ui/util/LifecycleEvents' ; import { LifecycleToggle } from '../../../../web/js/ui/util/LifecycleToggle' ; import * as semver from 'semver' ; import { ReleaseMetadatas } from "polar-release-metadata/src/ReleaseMetadatas" ; import { Devices } from "polar-shared/src/util/Devices" ; export class SplashEngine { private engine : Engine < UserFacts , SplashEventHandlers > ; constructor ( private facts , private eventHandlers , private readonly externalEngineState ? ) { const rules = { whatsNew : new WhatsNewRule ( ) , } ; this . engine = new Engine ( facts , rules , eventHandlers , externalEngineState ) ; } public run ( ) { this . engine . run ( ) ; } public getFacts ( ) { return this . engine . getFacts ( ) ; } public toExternalEngineState ( ) { return this . engine . toExternalEngineState ( ) ; } } /**
 * Splash engine that automatically persists and resumes from local storage.
 */ export class DefaultSplashEngine extends SplashEngine { constructor ( facts , eventHandlers ) { super ( facts , eventHandlers , LocalStorageExternalState . get ( ) ) ; } public run ( ) { super . run ( ) ; const externalState = this . toExternalEngineState ( ) ; LocalStorageExternalState . set ( externalState ) ; } } class LocalStorageExternalState { private static KEY = 'splash-engine-state' ; public static get ( ) { const value = localStorage . getItem ( this . KEY ) ; if ( value ) { return JSON . parse ( value ) ; } else { return undefined ; } } public static set ( state ) { localStorage . setItem ( this . KEY , JSON . stringify ( state ) ) ; } } export interface SplashEventHandlers extends EventHandlers { readonly onWhatsNew : ( ) => void ; readonly onNetPromoter : ( ) => void ; readonly onSuggestions : ( ) => void ; } export interface MutableUserFacts { /**
     * The time the datastore was created.
     */ datastoreCreated : ISODateTimeString ; /**
     * The currently running version.
     */ version : string ; } export interface UserFacts extends Readonly < MutableUserFacts > { } interface SuggestionsState { } class SuggestionsRule implements Rule < UserFacts , SplashEventHandlers , SuggestionsState > { public run ( facts , eventMap , state ? ) { if ( ! state ) { state = { } ; } const canShow = ( ) => canFireMajorRule ( facts , eventMap , eventMap . onSuggestions , 'splash-suggestions-skipped' ) ; if ( canShow ( ) ) { eventMap . onSuggestions . handler ( ) ; } return [ facts , state ] ; } } interface NetPromoterState { } class NetPromoterRule implements Rule < UserFacts , SplashEventHandlers , NetPromoterState > { public run ( facts , eventMap , state ? ) { if ( ! state ) { state = { } ; } const canShow = ( ) => canFireMajorRule ( facts , eventMap , eventMap . onNetPromoter , 'splash-nps-skipped' ) ; if ( canShow ( ) ) { eventMap . onNetPromoter . handler ( ) ; } return [ facts , state ] ; } } interface WhatsNewState { version : string ; } class WhatsNewRule implements Rule < UserFacts , SplashEventHandlers , WhatsNewState > { public run ( facts , eventMap , state ? ) { const hasUpdated = state && semver . lt ( state . version , facts . version ) ; const hasMetadata = ReleaseMetadatas . hasMetadata ( facts . version ) ; if ( hasUpdated && hasMetadata ) { if ( Devices . isDesktop ( ) || Devices . isTablet ) { eventMap . onWhatsNew . handler ( ) ; } } state = { version : facts . version } ; return [ facts , state ] ; } } param facts param eventMap param event param analyticsKey function canFireMajorRule ( facts , eventMap , event , analyticsKey ) { const hasExistingAgedDatastore = ( ) => { return UserFactsUtils . hasExistingAgedDatastore ( facts , '3d' ) ; } ; const hasMinimumTimeSinceLastEvent = ( ) => { const lastEventExecution = EventMaps . latestExecution ( eventMap ) ; return hasMinimumTimeSince ( lastEventExecution , '15m' ) ; } ; const hasMinimumTimeSinceLastRuleFired = ( ) => { const epoch = event . lastExecuted ; return hasMinimumTimeSince ( epoch , '7d' ) ; } ; if ( ! hasExistingAgedDatastore ( ) ) { return false ; } if ( ! hasMinimumTimeSinceLastMajorPrompt ( eventMap ) ) { return false ; } if ( ! hasMinimumTimeSinceLastEvent ( ) ) { return false ; } if ( ! hasMinimumTimeSinceLastRuleFired ( ) ) { return false ; } if ( ! hasTourTerminated ( ) ) { return false ; } return true ; } function hasMinimumTimeSinceLastMajorPrompt ( eventMap ) { const events = [ eventMap . onNetPromoter , eventMap . onSuggestions ] ; for ( const event of events ) { if ( ! hasMinimumTimeSince ( event . lastExecuted , '1d' ) ) { return false ; } } return true ; } function hasMinimumTimeSince ( epoch , duration , defaultValue = true ) { if ( epoch ) { const since = ISODateTimeStrings . parse ( epoch ) ; return TimeDurations . hasElapsed ( since , duration ) ; } else { return defaultValue ; } } function hasTourTerminated ( ) { return LifecycleToggle . isMarked ( LifecycleEvents . TOUR_TERMINATED ) ; } class UserFactsUtils { public static hasExistingAgedDatastore ( facts , duration ) { if ( facts . datastoreCreated ) { const since = ISODateTimeStrings . parse ( facts . datastoreCreated ) ; if ( TimeDurations . hasElapsed ( since , duration ) ) { return true ; } } return false ; } }