Reference Source

js/runtime/client_runtime.js

// NPM IMPORTS
import assert from 'assert'
import uuid from 'uuid'
import { fromJS } from 'immutable'

// COMMON IMPORTS
import Stream                 from '../../../node_modules/devapt-core-common/dist/js/messaging/stream'
import T                      from '../../../node_modules/devapt-core-common/dist/js/utils/types'
import Credentials            from '../../../node_modules/devapt-core-common/dist/js/base/credentials'
import ReduxStore             from '../../../node_modules/devapt-core-common/dist/js/state_store/redux_store'
import RuntimeBase            from '../../../node_modules/devapt-core-common/dist/js/base/runtime_base'
import {register_runtime}     from '../../../node_modules/devapt-core-common/dist/js/base/runtime'
import DefaultRenderingPlugin from '../../../node_modules/devapt-core-common/dist/js/default_plugins/rendering_default_plugin'
import RenderingPlugin        from '../../../node_modules/devapt-core-common/dist/js/plugins/rendering_plugin'

// BROWSER IMPORTS
import ConsoleLogger  from '../loggers/console_logger'
import StreamLogger   from '../loggers/stream_logger'
import Service        from './service'
import UI             from './ui'
import Router         from './router'
import DisplayCommand from '../commands/display_command'
import Component      from '../base/component'


let context = 'browser/runtime/client_runtime'



/**
 * @file client runtime class - main library interface.
 * 
 * @author Luc BORIES
 * 
 * @license Apache-2.0
 */
export default class ClientRuntime extends RuntimeBase
{
	/**
	 * Create a client Runtime instance.
	 * @extends RuntimeBase
	 * 
	 * 	API:
	 * 		->constructor()
	 * 		
	 * 		->get_session_uid():string - get unique session id.
	 * 		->get_session_credentials():Credentials - get session credentials instance.
	 * 
	 * 		->load(arg_settings):nothing - Load runtime settings.
	 * 
	 * 		->register_service(arg_svc_name, arg_svc_settings):Promise(Service) - Register a remote service.
	 * 		->service(arg_name):Service - Get a service by its name.
	 * 
	 * 		->command(arg_name):object - Get a command by its name.
	 * 
	 * 		->ping():nothing - Emit a ping request through SocketIO.
	 * 
	 * 		->get_state_store():object - Get state store, a Redux data store.(INHERITED)
	 * 		->get_store_reducers():function - Get reducer pure function: (previous state, action) => new state.
	 * 		->handle_store_change():nothing - Handle Redux store changes.
	 * 		->create_store_observer(arg_component):unsubscribe function - Create a store change observer.
	 * 
	 * 		->router():Router - Get runtime router.
	 * 
	 * @returns {nothing}
	 */
	constructor()
	{
		super(context)

		// INIT LOGGING FEATURE ON BROWSER
		const console_logger = new ConsoleLogger(true)
		this.get_logger_manager().loggers.push(console_logger)
		
		const stream_logger = new StreamLogger(undefined, true)
		this.get_logger_manager().loggers.push(stream_logger)

		stream_logger.get_stream().subscribe(
			(arg_log)=>{
				// console.log(context + ':stream_logger:logs', arg_log)
				if (this._state_store)
				{
					const action = {
						type:'ADD_JSON_LOGS',
						logs:arg_log.logs
					}
					this._state_store.dispatch(action)
				}
			}
		)

		this.logs_stream = stream_logger.get_stream()
		
		this.is_browser_runtime = true
		
		this.classes = {}
		this.classes.T = T
		this.classes.DefaultRenderingPlugin = DefaultRenderingPlugin
		this.classes.Component = Component
		this.classes.RenderingPlugin = RenderingPlugin

		this._session_credentials = undefined
		this._session_uid = uuid.v1()

		this._services = {}
		this._services_promises = {}
		this._ui = undefined
		this._router = undefined
		this._commands = undefined
		
		this.info('Client Runtime is created')

		// this.enable_trace()
		this.disable_trace()
		// this.update_trace_enabled()

		register_runtime(this)
	}



	/**
	 * Get session id.
	 * 
	 * @returns {string} - unique session id.
	 */
	get_session_uid()
	{
		return this._session_uid
	}



	/**
	 * Get session credentials.
	 * 
	 * @returns {Credentials} - session credentials instance.
	 */
	get_session_credentials()
	{
		return this._session_credentials
	}



	/**
	 * Get UI.
	 */
	ui()
	{
		return this._ui
	}



	/**
	 * Get UI rendering helper.
	 */
	ui_rendering()
	{
		return this._ui._ui_rendering
	}



	/**
	 * 
	 */
	shoud_log_bindingd_stream()
	{
		return false
	}



	/**
	 * Get application initial state: from browser cache or from DOM script.
	 * 	State strategy is {
	 * 		source:'browser' or 'session' or 'html',
	 * 		save_period: 5000, milliseconds between two state saves, 0 to disable save
	 * 		state_key: '...' name of the store key which corresponding value contains the key name of the application state.
	 * 	}
	 * 
	 * @param {object} arg_app_state_strategy - strategy to manage application state.
	 * 
	 * @returns {object}
	 */
	get_app_initial_state(arg_app_state_strategy)
	{
		const browser_supports_storage = (typeof(Storage) !== "undefined")
		const source = ( T.isObject(arg_app_state_strategy) && browser_supports_storage) ? arg_app_state_strategy.source : 'html'
		const app_state_key = T.isObject(arg_app_state_strategy) && T.isString(arg_app_state_strategy.state_key) ? arg_app_state_strategy.state_key : '__DEVAPT_APP_STATE_KEY__'
		
		let state = undefined
		const window_state = window ? window.__INITIAL_STATE__ : {error:'no browser window object'}
		switch(source) {
			case 'browser':{
				const store_key = localStorage.getItem(app_state_key)
				if (! T.isString(store_key) )
				{
					state = window_state
					break
				}
				const state_str = localStorage.getItem(store_key)
				// console.log('get_app_initial_state:state_str', typeof state_str)
				state = T.isString(state_str) ? JSON.parse(state_str) : window_state
				break
			}
			case 'session':{
				const store_key = sessionStorage.getItem(app_state_key)
				if (! T.isString(store_key) )
				{
					state = window_state
					break
				}
				const state_str = sessionStorage.getItem(store_key)
				// console.log('get_app_initial_state:state_str', typeof state_str)
				state = T.isString(state_str)  ? JSON.parse(state_str) : window_state
				break
			}
			case 'html':{
				state = window_state
				break
			}
		}

		return state ? state : {error:'no app state found'}
	}
	


	/**
	 * Configure application state save: to browser local or session storage.
	 * 	State strategy is {
	 * 		source:'browser' or 'session' or 'html',
	 * 		save_period: 5000, milliseconds between two state saves, 0 to disable save
	 * 		state_key: '...' name of the store key which corresponding value contains the key name of the application state.
	 * 	}
	 * 
	 * @returns {nothing}
	 */
	init_app_state_save()
	{
		this.enter_group('init_app_state_save')
		
		// GET AND CHECK PERIOD
		let period = this.app_state_strategy.save_period
		if (period == 0 || ! T.isNumber(period) )
		{
			this.leave_group('init_app_state_save:disabled with not period > 0')
			return
		}
		if (period < 3000)
		{
			period = 3000
		}

		// CHECK STORAGE BROWSER SUPPORT
		const browser_supports_storage = (typeof(Storage) !== "undefined")
		if (! browser_supports_storage)
		{
			this.error('init_app_state_save:no storage support')
			this.leave_group('init_app_state_save:error')
			return
		}

		// CHECK STRATEGY
		if (! T.isObject(this.app_state_strategy) )
		{
			this.error('init_app_state_save:no state save strategy')
			this.leave_group('init_app_state_save:error')
			return
		}
		
		// GET ATTRIBUTES
		const source = this.app_state_strategy.source
		const app_state_key = T.isString(this.app_state_strategy.state_key) ? this.app_state_strategy.state_key : '__DEVAPT_APP_STATE_KEY__'
		
		const state = this._state_store.get_state()
		const state_app = T.isObject(state) && state.getIn ? state.getIn(['credentials', 'application'], undefined) : undefined
		const app = T.isString(state_app) ? state_app.toLocaleUpperCase() : 'NO_APP_NAME'
		const default_store_key = '__DEVAPT_APP_STATE_' + app + '__'

		// GET SAVE CALLBACK
		let save_cb = undefined
		switch(source) {
			case 'browser':{
				let store_key = localStorage.getItem(app_state_key)
				if (! T.isString(store_key) )
				{
					store_key = default_store_key
					localStorage.setItem(app_state_key, store_key)
				}
				save_cb = ()=>{
					const state_str = JSON.stringify(this._state_store.get_state().toJS())
					localStorage.setItem(store_key, state_str)
				}
				break
			}
			case 'session':{
				let store_key = sessionStorage.getItem(app_state_key)
				if (! T.isString(store_key) )
				{
					store_key = default_store_key
					sessionStorage.setItem(app_state_key, store_key)
				}
				save_cb = ()=>{
					const state_str = JSON.stringify(this._state_store.get_state().toJS())
					sessionStorage.setItem(store_key, state_str)
				}
				break
			}
		}

		// REGISTER PERIODICAL SAVES
		if (save_cb)
		{
			this.info('init_app_state_save:register saves with period=%s', period)
			setTimeout(save_cb, period)
		}

		this.leave_group('init_app_state_save')
	}

	
	
	/**
	 * Load runtime settings.
	 * 
	 * @param {object} arg_settings - runtime settings.
	 * 
	 * @returns {nothing}
	 */
	load(arg_settings)
	{
		this.separate_level_1()
		this.enter_group('load')

		// SET DEFAULT REMOTE LOGGING
		// const svc_logger_settings = ('default' in arg_settings) ? arg_settings['default'] : {}
		// this.loggers.push( new LoggerSvc(true, svc_logger_settings) )
		
		// GET INITIAL STATE
		const initial_app_state = this.get_app_initial_state(arg_settings.app_state_strategy)
		// this.debug('initialState', initial_app_state)
		this.app_state_strategy = arg_settings.app_state_strategy
		
		// GET DEFAULT REDUCER
		if ( T.isFunction(arg_settings.reducers) )
		{
			this.default_reducer = arg_settings.reducers
		}
		else
		{
			this.default_reducer = (arg_previous_state/*, arg_action*/) => {
				return arg_previous_state
			}
		}
		
		// CREATE STATE STORE
		const reducer = this.get_store_reducers()
		const self = this
		this._state_store = new ReduxStore(reducer, initial_app_state, context, this.get_logger_manager())
		this._state_store_unsubscribe = this._state_store.subscribe( self.handle_store_change.bind(self) )
		this._state_store.dispatch( {type:'store_created'} )
		
		// CREATE CREDENTIALS INSTANCE
		const credentials_settings = this._state_store.get_state().get('credentials', undefined)
		this.debug('credentials_settings', credentials_settings)

		const credentials_update_handler = (arg_credentials_map)=>{
			this._state_store.dispatch( {type:'SET_CREDENTIALS', credentials:arg_credentials_map } )
		}
		const credentials_datas = credentials_settings ? credentials_settings.toJS() : Credentials.get_empty_credentials()
		this._session_credentials = new Credentials(credentials_datas, credentials_update_handler)

		// CREATE UI WRAPPER
		this._ui = new UI(this, this._state_store)

		// CREATE NAVIGATION ROUTER
		this._router = new Router()

		this._ui.load()


		// ADD COMMANDS ROUTE
		const add_cmd_cb = ()=>{
			this._commands = this._state_store.get_state().get('commands', {}).toJS()
			this._commands_instances = {}
			Object.keys(this._commands).forEach(
				(cmd_name)=>{
					const cmd = this._commands[cmd_name]

					if (cmd.type != 'display')
					{
						console.warn('load:bad cmd [' + cmd_name + '] with type:' + cmd.type)
						return
					}

					cmd.name = cmd_name
					if (! (cmd_name in this._commands_instances) )
					{
						this._commands_instances[cmd_name] = new DisplayCommand(this, cmd)
					}

					const display_command = this._commands_instances[cmd_name]
					if (! display_command.is_valid())
					{
						this.error('load:no route handler for cmd [' + cmd_name + ']:no valid command settings')
						console.error('load:no route handler for cmd [' + cmd_name + ']:no valid command settings')
						return
					}

					const route = display_command.get_route()
					this.debug('load:add route handler for cmd [' + cmd_name + '] with route:' + route)
					// console.debug(context + ':load:add route handler for cmd [' + cmd_name + '] with route:' + route)

					this._router.add_handler(route,
						()=>{
							// console.debug(context + ':load:execute handler for cmd [' + cmd_name + '] with route:' + route)
							this._ui.pipe_display_command(display_command)
						}
					)
				}
			)
		}
		
		add_cmd_cb()

		// ENABLE HASH HANDLING
		this._router.init()


		this.leave_group('load')
		this.separate_level_1()
	}
	
	
	
	/**
	 * Register a remote service.
	 * 
	 * @param {string} arg_svc_name - service name.
	 * @param {object} arg_svc_settings - service settings.
	 * 
	 * @returns {Promise} - Promise(Service)
	 */
	register_service(arg_svc_name, arg_svc_settings)
	{
		// this.enable_trace()
		const self = this
		this.enter_group('register_service:' + arg_svc_name)
		
		if (arg_svc_name in this._services_promises)
		{
			this.leave_group('register_service:svc promise found for ' + arg_svc_name)
			return this._services_promises[arg_svc_name]
		}

		this.debug('register_service:create svc promise:' + arg_svc_name)
		this._services_promises[arg_svc_name] = new Promise(
			function(resolve, reject)
			{
				self.register_service_self(resolve, reject, arg_svc_name, arg_svc_settings)
			}
		)
		.then(
			(service)=>{
				this.leave_group('register_service:svc promise created for ' + arg_svc_name)
				return service
			}
		)
		
		this.info('Client Service is created (async):' + arg_svc_name)
		// console.info('Client Service is created (async):' + arg_svc_name)
	
		this.leave_group('register_service:async')
		return this._services_promises[arg_svc_name]
	}

	
	
	/**
	 * Register a remote service (end of process).
	 * 
	 * @param {string} arg_svc_name - service name.
	 * @param {object} arg_svc_settings - service settings.
	 * @param {Function} arg_resolve_cb - function to call when promise is resolved.
	 * @param {Function} arg_reject_cb - function to call when promise is rejected.
	 * 
	 * @returns {nothing}
	 */
	register_service_self(arg_resolve_cb, arg_reject_cb, arg_svc_name, arg_svc_settings)
	{
		const self = this

		// this.enter_group('register_service_self')
		

		// const app_credentials = this._state_store.get_state().get('credentials')
		const app_credentials = this._session_credentials.get_credentials()

		// CHECK SERVICE NAME
		if ( ! T.isString(arg_svc_name) )
		{
			this.error('register_service:svc promise rejected:' + arg_svc_name)
			arg_reject_cb(context + ':register_service:bad service name string [' + arg_svc_name + ']')
			return
		}

		// TEST IF SERVICE IS ALREADY REGISTERED
		if (this._services && (arg_svc_name in this._services) )
		{
			this.debug('register_service_self:SERVICE IS ALREADY REGISTERED for ' + arg_svc_name)

			const svc = this._services[arg_svc_name]
			
			// console.log(context + ':register_service_self:SERVICE IS ALREADY REGISTERED:svc', svc)
			this.debug('register_service:svc promise resolved:' + arg_svc_name)
			
			arg_resolve_cb(svc)
			
			// this.leave_group('register_service_self')
			return
		}

		// GET SERVICE SETTINGS FROM GIVEN SETTINGS
		if ( T.isObject(arg_svc_settings) )
		{
			this.debug('register_service_self:SERVICE FROM GIVEN SETTINGS for ' + arg_svc_name)
			// console.log(context + ':register_service_self:SERVICE FROM GIVEN SETTINGS:arg_svc_settings', arg_svc_settings)

			// GET APPLICATION CREDENTIALS
			// TODO CHECK CREDENTIAL FORMAT STRING -> MAP ?
			arg_svc_settings.credentials = app_credentials
			arg_svc_settings.session_uid = this.get_session_uid()
			
			// console.log(context + ':register_service_self:credentials', arg_svc_settings.credentials = app_credentials)
			
			if ( T.isString(arg_svc_settings.credentials ) )
			{
				arg_svc_settings.credentials = JSON.parse(arg_svc_settings.credentials)
			}
			const svc = new Service(arg_svc_name, arg_svc_settings)
			
			// console.log(context + ':register_service_self:SERVICE FROM GIVEN SETTINGS:svc', svc)
			
			self._services[arg_svc_name] = svc
			this.debug('register_service:svc promise resolved:' + arg_svc_name)
			arg_resolve_cb(svc)

			// this.leave_group('register_service_self')
			return
		}

		// GET SERVICE SETTINGS FROM SERVER SETTINGS: PROCESS RESPONSE
		
		const svc_path = '/topology'
		const request_svc_settings = 'devapt-deployed-service-infos'
		const reply_svc_settings = 'devapt-deployed-service-infos'
		const svc_socket = window.io(svc_path)
		const get_settings_stream = Stream.from_emitter_event(svc_socket, reply_svc_settings)
		
		
		// GET SERVICE SETTINGS FROM SERVER SETTINGS: REQUEST SETTINGS
		
		// DEFINE REQUEST PAYLOAD
		const request = {
			session_uid:this.get_session_uid(),
			service:'topology',
			operation:request_svc_settings,
			operands: [app_credentials.tenant, arg_svc_name],
			credentials:app_credentials
		}
		this.debug('register_service_self:emit with operation=' + request_svc_settings + ' and operation=', request.operation, request.credentials)
		svc_socket.emit(request_svc_settings, request)


		this.debug('register_service_self:SERVICE FROM SERVER SETTINGS for ' + arg_svc_name)
		get_settings_stream.subscribe(
			(response) => {
				// console.log(context + ':register_service_self:SERVICE FROM SERVER SETTINGS:response', response)
				
				if (response.has_error)
				{
					arg_reject_cb(response.error)
					self.debug('register_service:svc promise rejected:' + arg_svc_name + ' with error=' + response.error)
					return
				}

				arg_svc_settings = response.results
				assert( T.isObject(arg_svc_settings), context + ':register_service:bad service settings object')
				self.debug('register_service_self:SERVICE FROM SERVER SETTINGS:arg_svc_settings', arg_svc_settings)

				// GET APPLICATION CREDENTIALS
				// TODO CHECK CREDENTIAL FORMAT STRING -> MAP ?
				arg_svc_settings.credentials = app_credentials
				arg_svc_settings.session_uid = this.get_session_uid()
				if ( T.isString(arg_svc_settings.credentials ) )
				{
					arg_svc_settings.credentials = JSON.parse(arg_svc_settings.credentials)
				}

				const svc = new Service(arg_svc_name, arg_svc_settings)
				self.debug('register_service_self:SERVICE FROM SERVER SETTINGS:svc', svc)

				self._services[arg_svc_name] = svc
				delete self._services_promises[arg_svc_name]

				self.debug('register_service:svc promise resolved:' + arg_svc_name)
				arg_resolve_cb(svc)
			}
		)

		get_settings_stream.on_error(
			(error) => {
				self.error('register_service:svc promise rejected:' + arg_svc_name + ' with error:' + error)
				arg_reject_cb(context + ':register_service:request error for  [' + arg_svc_name + '] error=' + error)
			}
		)

		

		// this.leave_group('register_service_self')
	}
	
	
	
	/**
	 * Get a service by its name.
	 * 
	 * @param {string} arg_name - service name.
	 * 
	 * @returns {Service}
	 */
	service(arg_name)
	{
		// console.info('getting/creating service', arg_name)
		return (arg_name in this._services) ? this._services[arg_name] : undefined
	}
	
	
	
	/**
	 * Get a command by its name.
	 * 
	 * @param {string} arg_name - command name.
	 * 
	 * @returns {object}
	 */
	command(arg_name)
	{
		// console.info('getting/creating service', arg_name)
		return (arg_name in this._commands) ? this._commands[arg_name] : undefined
	}
	

	
	/**
	 * Emit a ping request through SocketIO.
	 * 
	 * @returns {nothing}
	 */
	ping()
	{
		const socketio = window.io()
		socketio.emit('ping')
	}
	
	
	
	/**
	 * Get store reducers.
	 * 
	 * !!! Do not trace with Loggable.* into reducers with an enabled StreamLogger instance
	 *  because it dispatch its received trace to update app state.
	 * 
	 * @returns {function} - reducer pure function: (previous state, action) => new state
	 */
	get_store_reducers()
	{
		return (arg_previous_state, arg_action) => {
			// console.info('reducer 1:type=' + arg_action.type + ' for ' + arg_action.component)
			
			// ADD LOG RECORD
			if ( T.isString(arg_action.type) && arg_action.type == 'ADD_JSON_LOGS' && T.isArray(arg_action.logs) )
			{
				const path = ['logs', 'state', 'items']
				const logs = arg_previous_state.getIn(path, []).concat(arg_action.logs)
				return arg_previous_state.setIn(path, logs)
			}

			// SET PAGE CONTENT
			if (T.isString(arg_action.type) && arg_action.type ==  'SET_PAGE_CONTENT' && T.isString(arg_action.resource) && arg_action.resource == 'content')
			{
				if ( T.isArray(arg_action.content_body) )
				{
					const body_contents_path = ['views', 'content', 'state', 'body_contents']
					return arg_previous_state.setIn(body_contents_path, fromJS(arg_action.content_body) )
				}
			}

			// ADD JSON RESOURCE SETTINGS
			if ( T.isString(arg_action.type) && arg_action.type == 'ADD_JSON_RESOURCE' && T.isString(arg_action.resource) && T.isObject(arg_action.json) )
			{
				// console.log('reducer:ADD_JSON_RESOURCE', arg_action.resource, arg_action.json)

				if ( T.isString(arg_action.collection) )
				{
					if ( T.isArray(arg_action.path) )
					{
						return arg_previous_state.setIn([arg_action.collection, arg_action.resource].concat(arg_action.path), fromJS(arg_action.json) )
					}

					return arg_previous_state.setIn([arg_action.collection, arg_action.resource], fromJS(arg_action.json) )
				}

				if ( T.isArray(arg_action.path) )
				{
					return arg_previous_state.setIn(arg_action.path, fromJS(arg_action.json) )
				}
			}

			// SET SESSION CREDENTIALS
			if ( T.isString(arg_action.type) && arg_action.type == 'SET_CREDENTIALS' && T.isObject(arg_action.credentials) )
			{
				// console.log('reducer:SET_CREDENTIALS', arg_action.credentials)
				return arg_previous_state.set('credentials', arg_action.credentials)
			}

			// DISPATCH TO COMPONENTS REDUCERS
			if ( T.isString(arg_action.component) )
			{
				// console.info(context + ':reducer 2:type=' + arg_action.type + ' for ' + arg_action.component)
				const component = this._ui.get(arg_action.component)
				
				if ( T.isObject(component) && component.is_component )
				{
					// console.info(context + ':reducer 3:type=' + arg_action.type + ' for ' + arg_action.component, component)
					
					if ( T.isFunction(component.reduce_action) )
					{
						// console.info(context + ':reducer 4:type=' + arg_action.type + ' for ' + arg_action.component)
						
						// console.log(context + ':reducer 4:path', component.get_state_path())
						// console.log(context + ':reducer 4:arg_previous_state', arg_previous_state.toJS())
						
						const prev_component_state = arg_previous_state.getIn(component.get_state_path())
						if (! prev_component_state)
						{
							console.log(context + ':reducer:arg_previous_state', arg_previous_state.toJS())
							console.log(context + ':reducer:component.get_state_path()', component.get_state_path())
							console.error(context + ':reducer:bad prev_component_state', prev_component_state)
							return fromJS( { error:'no state' } )
						}

						// console.log(context + ':reducer 4:prev_component_state', prev_component_state.toJS())
						
						let new_component_state = component.reduce_action(prev_component_state, arg_action)
						if (new_component_state && new_component_state != prev_component_state)
						{
							const prev_state_version = prev_component_state.get('state_version', 0)
							new_component_state = new_component_state.set('state_version', prev_state_version + 1)
						}

						// console.log(context + ':reducer 4:new_component_state', new_component_state.toJS())
						
						let state = this._state_store.get_state()
						state = state.setIn(component.get_state_path(), new_component_state)
						// console.log(context + ':reducer 4:state', state.toJS())
						
						return state
					}
				}
			}
			return this.default_reducer(arg_previous_state, arg_action)
		}
	}
	
	
	
	/**
	 * Handle Redux store changes.
	 * 
	 * !! Do not trace with Loggable.* here.
	 * 
	 * @returns {nothing}
	 */
	handle_store_change()
	{
		// let previous_state = this.current_state
		// this.current_state = this._state_store.get_state()
		
		/// TODO
		
		// this.info('handle_store_change:global', this._state_store.get_state())
	}
	
	
	
	/**
	 * Create a store change observer.
	 * 
	 * @param {Component} arg_component - component instance.
	 * 
	 * @returns {function} - store unsubscribe function.
	 */
	create_store_observer(arg_component)
	{
		assert( T.isObject(arg_component) && arg_component.is_component, context + ':create_store_observer:bas component object')
		
		let current_state = undefined
		
		const handle_change = () => {
			let next_state = arg_component.get_state()
			// console.log(context + ':handle_change:arg_component', arg_component)
			// console.log(context + ':handle_change:next_state', next_state)

			if ( next_state && ! next_state.equals(current_state) )
			{
				// console.info(context + ':create_store_observer:state changes for ' + arg_component.get_name())
				
				arg_component.handle_state_change(current_state, next_state)
				current_state = next_state
			}
			// else
			// {
			// 	console.info(context + ':create_store_observer:no state change for ' + arg_component.get_name())
			// }
		}
		
		let unsubscribe = this._state_store.subscribe(handle_change)
		
		handle_change()
		
		return unsubscribe
	}



	/**
	 * Get runtime router.
	 * 
	 * @returns {Router}
	 */
	router()
	{
		assert( T.isObject(this._router), context + ':router:bad router object')
		return this._router
	}
}