/**
 * Copyright 2018 Twitter, Inc.
 * Licensed under the Apache License, Version 2.0
 * http://www.apache.org/licenses/LICENSE-2.0
 */ import { SqrlObject } from "../object/SqrlObject" ; import bluebird = require ( "bluebird" ) ; import { foreachObject } from "../jslib/foreachObject" ; import invariant from "../jslib/invariant" ; import isPromise from "../jslib/isPromise" ; import util = require ( "util" ) ; import { niceForEach } from "node-nice" ; import SqrlSourcePrinter from "../compile/SqrlSourcePrinter" ; import { RuleSpecMap } from "../api/spec" ; import * as moment from "moment" ; import { isValidFeatureName } from "../feature/FeatureName" ; import { DatabaseSet , Context } from "../api/ctx" ; import { Manipulator , ExecutionErrorProperties , FeatureMap , Execution , } from "../api/execute" ; import { SqrlBoxed } from "sqrl-common" ; import { SourcePrinter } from "../api/executable" ; export interface SqrlExecutionOptions { featureTimeout : number ; ruleSpecs : RuleSpecMap ; requiredSlots ? : number [ ] ; sourcePrinter ? : SqrlSourcePrinter ; slotCost ? : number [ ] ; } export class SlotMissingCallbackError extends Error { constructor ( public slotName ) { super ( `Could not find function to generate slot value:: ${ slotName }` ) ; } } export class SqrlExecutionState implements Execution { slots : bluebird < any > [ ] ; names : string [ ] ; functionCache ; counterBumps ; loggedCodedErrors : Set < string > ; sourcePrinter : SqrlSourcePrinter ; featureTimeout : number ; currentCost : number = 0 ; public traceFunctions : boolean ; public databaseSet : DatabaseSet | null ; private clockMs : number = null ; public ruleSpecs : RuleSpecMap ; public ctx : Context ; private data = new Map ( ) ; _fetch : ( slot : number ) => bluebird < any > ; constructor ( ctx , slotCallbacks , names = [ ] , public manipulator = null , props , features ) { this . ctx = ctx ; this . featureTimeout = props . featureTimeout ; this . slots = Array ( slotCallbacks . length ) ; this . counterBumps = [ ] ; this . names = names ; this . functionCache = { } ; this . loggedCodedErrors = new Set ( ) ; this . databaseSet = ctx . requireDatabaseSet ( ) ; this . ruleSpecs = props . ruleSpecs ; this . sourcePrinter = props . sourcePrinter || null ; const slotCost = props . slotCost ; this . _fetch = ( slotIdx ) => { const slotCalc = slotCallbacks [ slotIdx ] ; if ( ! slotCalc ) { throw new SlotMissingCallbackError ( this . names [ slotIdx ] ) ; } this . currentCost += slotCost ? slotCost [ slotIdx ] : 0 ; return slotCalc . call ( this ) ; } ; this . setFeatures ( features ) ; ( props . requiredSlots || [ ] ) . forEach ( ( index ) => { invariant ( this . slots [ index ] , "Slot %s required for execution" , this . names [ index ] ) ; } ) ; } /**
     * Fetch a value from the executions data cache
     */ get < T > ( symbol , defaultValue ? ) { if ( ! this . data . has ( symbol ) ) { if ( typeof defaultValue !== "undefined" ) { return defaultValue ; } throw new Error ( "Could not find symbol in executions data: " + symbol . toString ( ) ) ; } return this . data . get ( symbol ) ; } /**
     * Fetch a value from the executions data cache
     */ set < T > ( symbol , value ) { this . data . set ( symbol , value ) ; } /**
     * Get a value on the executions data, setting a default value if not set
     */ setDefault < T > ( symbol , defaultValue ? ) { if ( ! this . data . has ( symbol ) ) { this . data . set ( symbol , defaultValue ) ; } return this . data . get ( symbol ) ; } async fetchClock ( ) { const clockValue = await this . fetchBasicByName ( "SqrlClock" ) ; invariant ( clockValue !== null , "fetchClock() could not fetch the SqrlClock feature" ) ; const clockMoment = moment ( clockValue , moment . ISO_8601 ) ; invariant ( clockMoment . isValid ( ) , "Invalid ISO 8601 string provided as SqrlClock" ) ; this . clockMs = clockMoment . valueOf ( ) ; } /**
     * Get the source code printer for the executable.
     */ getSourcePrinter ( ) { return this . sourcePrinter ; } getClock ( ) { invariant ( this . clockMs !== null , "Clock not fetched or unavailable" ) ; return this . clockMs ; } getClockMs ( ) { return this . getClock ( ) ; } async fetchFeature ( featureName ) { const value = await this . fetchByName ( featureName ) ; if ( value instanceof SqrlObject ) { return value ; } else { return new SqrlBoxed ( value ) ; } } fetchValue ( featureName ) { return Promise . resolve ( this . fetchBasicByName ( featureName ) ) ; } functionCacheAccessor < T > ( key , callback ) { if ( ! this . functionCache . hasOwnProperty ( key ) ) { this . functionCache [ key ] = callback ( ) ; } return this . functionCache [ key ] ; } cacheAccessor < T > ( symbol , key , callback ) { this . functionCache [ symbol ] = this . functionCache [ symbol ] || { } ; if ( ! this . functionCache [ symbol ] . hasOwnProperty ( key ) ) { this . functionCache [ symbol ] [ key ] = callback ( ) ; } return this . functionCache [ symbol ] [ key ] ; } trace ( props , format , ... args ) { return this . ctx . trace ( props , format , ... args ) ; } debug ( props , format , ... args ) { return this . ctx . debug ( props , format , ... args ) ; } info ( props , format , ... args ) { return this . ctx . info ( props , format , ... args ) ; } warn ( props , format , ... args ) { return this . ctx . warn ( props , format , ... args ) ; } error ( props , format , ... args ) { return this . ctx . error ( props , format , ... args ) ; } fatal ( props , format , ... args ) { return this . ctx . fatal ( props , format , ... args ) ; } codedWarning ( format , ... args ) { const message = util . format ( format , ... args ) ; if ( this . manipulator ) { this . manipulator . codedWarnings . push ( message ) ; } this . warn ( { } , message ) ; } setFeatures ( featureMap ) { foreachObject ( featureMap , ( value , featureName ) => { const index = this . getSlot ( featureName ) ; invariant ( ! isPromise ( value ) , "setFeatures() does not work with promises" ) ; invariant ( ! this . slots [ index ] , "setFeatures() cannot overwrite loaded slots" ) ; this . slots [ index ] = bluebird . resolve ( value ) ; } ) ; } setFutureFeatures ( featureMap ) { foreachObject ( featureMap , ( promise , featureName ) => { const index = this . getSlot ( featureName ) ; invariant ( promise instanceof bluebird , "setFutureFeatures() requires bluebird promises" ) ; invariant ( ! this . slots [ index ] , "setFutureFeatures() cannot overwrite loaded slots" ) ; this . slots [ index ] = promise ; } ) ; } fetch ( slot ) { let value = this . slots [ slot ] ; if ( ! value ) { value = this . _fetch ( slot ) ; this . slots [ slot ] = value ; } return value ; } getFeatureNames ( ) { return this . names . filter ( isValidFeatureName ) ; } getSlot ( name ) { invariant ( typeof name === "string" , "Expected string name for getSlot()" ) ; const index = this . names . indexOf ( name ) ; invariant ( index >= 0 , "Could not find the requested name:: %s" , name ) ; return index ; } async tryWait ( name ) { const index = this . names . indexOf ( name ) ; if ( index > 0 ) { await this . build ( [ index ] ) ; } } fetchByName ( name ) { return bluebird . resolve ( this . fetchByName_ ( name ) ) ; } async fetchByName_ ( name ) { return this . fetch ( this . getSlot ( name ) ) ; } fetchBasicByName ( name ) { return this . fetchByName ( name ) . then ( ( result ) => SqrlObject . ensureBasic ( result ) ) ; } prepare ( slotIndexes ) { return bluebird . resolve ( this . prepare_ ( slotIndexes ) ) ; } prepare_ ( slotIndexes ) { return niceForEach ( slotIndexes , ( idx ) => { if ( ! this . slots [ idx ] ) { this . slots [ idx ] = this . _fetch ( idx ) ; } } ) ; } wait ( slotIndexes ) { let currentSlotIndex = 0 ; const wait = ( ) => { while ( currentSlotIndex < slotIndexes . length ) { const slot = this . slots [ slotIndexes [ currentSlotIndex ++ ] ] ; if ( ! slot . isResolved ( ) ) { return slot . then ( wait ) ; } } } ; return wait ( ) || bluebird . resolve ( ) ; } build ( slotIndexes ) { return this . prepare ( slotIndexes ) . then ( ( ) => this . wait ( slotIndexes ) ) ; } load ( slotIndexes ) { return this . prepare ( slotIndexes ) . then ( ( ) => { return bluebird . all ( slotIndexes . map ( ( idx ) => this . slots [ idx ] ) ) ; } ) ; } loadByNames ( featureNames ) { return this . load ( featureNames . map ( ( name ) => this . getSlot ( name ) ) ) ; } logCodedErrorMessage ( format , ... args ) { const msg = util . format ( format , ... args ) ; if ( this . loggedCodedErrors . has ( msg ) ) { return ; } this . loggedCodedErrors . add ( msg ) ; this . ctx . warn ( { } , `CodedError during sqrl execution:: ${ msg }` ) ; } logError ( err , props = { } ) { const errorType = ( err as any ) . code || err . name || "other" ; if ( this . manipulator ) { this . manipulator . logError ( err , props ) ; } let msg ; if ( props . functionName ) { msg = `Error in sqrl function ${ props . functionName }: ${ err . toString ( ) }` ; } else { msg = `Error in sqrl execution: ${ err . toString ( ) }` ; } const errorProps = Object . assign ( { err , errorType , } , props ) ; if ( props . fatal ) { this . fatal ( errorProps , "Fatal error: " + msg ) ; } else { this . error ( errorProps , msg ) ; } } getNamedGlobals ( ) { const sortedNames = Array . from ( this . names ) . sort ( ) ; return sortedNames . filter ( ( name ) => ! name . startsWith ( "ast:" ) ) ; } }