diff --git a/hugvey/central_command.py b/hugvey/central_command.py index f5b60f4..90555ae 100644 --- a/hugvey/central_command.py +++ b/hugvey/central_command.py @@ -17,13 +17,33 @@ from hugvey.story import Story from hugvey.voice.google import GoogleVoiceClient from hugvey.voice.player import Player from hugvey.voice.streamer import AudioStreamer +import queue logger = logging.getLogger("command") +# def exceptionEmitter(a): +# print(a) +# def decorate(func): +# print('decorate') +# async def call(*args, **kwargs): +# print('call') +# # pre(func, *args, **kwargs) +# try: +# result = await func(*args, **kwargs) +# except Exception as e: +# logger.critical(e, "in", func) +# raise e +# # post(func, *args, **kwargs) +# return result +# return call +# return decorate + + class CentralCommand(object): """docstring for CentralCommand.""" - def __init__(self, debug_mode = False): + + def __init__(self, debug_mode=False): self.debug = debug_mode self.eventQueue = asyncio.Queue() self.commandQueue = asyncio.Queue() @@ -31,39 +51,67 @@ class CentralCommand(object): self.hugveys = {} self.ctx = Context.instance() self.hugveyLock = asyncio.Lock() - + self.start_time = time.time() def loadConfig(self, filename): if hasattr(self, 'config'): raise Exception("Overriding config not supported yet") - + with open(filename, 'r') as fp: logger.debug('Load config from {}'.format(filename)) self.config = yaml.safe_load(fp) - - self.hugvey_ids = [i+1 for i in range(self.config['hugveys'])] - + + self.hugvey_ids = [i + 1 for i in range(self.config['hugveys'])] + # load languages: self.languages = {} - + for lang in self.config['languages']: with open(lang['file'], 'r') as fp: self.languages[lang['code']] = yaml.load(fp) - + self.panopticon = Panopticon(self, self.config) - - + + def getHugveyStatus(self, hv_id): + status = {'id': hv_id} + if not hv_id in self.hugveys: + status['status'] = 'off' + return status + + hv = self.hugveys[hv_id] + status['status'] = 'running' if hv.isRunning.is_set() else 'paused' + status['language'] = hv.language_code + status['msg'] = hv.story.currentMessage.id + status['counts'] = hv.story.getStoryCounts() + status['finished'] = hv.story.isFinished() + + + return status + + def getStatusSummary(self): + status = { + 'uptime': time.time() - self.start_time, + 'languages': self.config['languages'], + 'hugveys': [], + } + + for hv_id in self.hugvey_ids: + status['hugveys'].append(self.getHugveyStatus(hv_id)) + + return status + def commandHugvey(self, hv_id, msg): """ prepare command to be picked up by the sender """ if threading.current_thread().getName() != 'MainThread': # Threading nightmares! Adding to queue from other thread/loop (not sure which is the isse) - # won't trigger asyncios queue.get() so we have to do this thread safe, in the right loop - self.loop.call_soon_threadsafe( self._queueCommand, hv_id, msg ) + # won't trigger asyncios queue.get() so we have to do this thread + # safe, in the right loop + self.loop.call_soon_threadsafe(self._queueCommand, hv_id, msg) else: self._queueCommand(hv_id, msg) - + def _queueCommand(self, hv_id, msg): self.commandQueue.put_nowait((hv_id, msg)) # if msg['action'] == 'play': @@ -72,155 +120,166 @@ class CentralCommand(object): # 'msg': "This is an interrption", # 'id': 'test', # })) - + def commandAllHugveys(self, msg): for hv_id in self.hugvey_ids: self.commandHugvey(hv_id, msg) - + def commandAllActiveHugveys(self, msg): for hv_id in self.hugveys: self.commandHugvey(hv_id, msg) - + async def commandSender(self): s = self.ctx.socket(zmq.PUB) s.bind(self.config['events']['cmd_address']) - + self.commandAllHugveys({'action': 'show_yourself'}) - + # sleep to allow pending connections to connect await asyncio.sleep(1) - logger.info("Ready to publish commands on: {}".format(self.config['events']['cmd_address'])) - logger.debug('Already {} items in queue'.format(self.commandQueue.qsize())) - + logger.info("Ready to publish commands on: {}".format( + self.config['events']['cmd_address'])) + logger.debug('Already {} items in queue'.format( + self.commandQueue.qsize())) + while self.isRunning.is_set(): hv_id, cmd = await self.commandQueue.get() logger.info('Got command to send: {} {}'.format(hv_id, cmd)) zmqSend(s, hv_id, cmd) - + logger.warn('Stopping command sender') s.close() - + async def instantiateHugvey(self, hugvey_id, msg): ''' Start a HugveyState, according to a show_yourself reply - + 'event': 'connection', 'id': self.hugvey_id, 'host': socket.gethostname(), 'ip': self.getIp(), ''' - async with self.hugveyLock: # lock to prevent duplicates on creation + async with self.hugveyLock: # lock to prevent duplicates on creation if not hugvey_id in self.hugveys: logger.info(f'Instantiate hugvey #{hugvey_id}') + print('a') h = HugveyState(hugvey_id, self) - h.config(msg['host'],msg['ip']) + print('a') + h.config(msg['host'], msg['ip']) + print('b') self.hugveys[hugvey_id] = h - thread = threading.Thread(target=h.start, name=f"hugvey#{hugvey_id}") + thread = threading.Thread( + target=h.start, name=f"hugvey#{hugvey_id}") thread.start() + print('c') else: logger.info(f'Reconfigure hugvey #{hugvey_id}') # (re)configure exisitng hugveys - h.config(msg['host'],msg['ip']) - - - + h.config(msg['host'], msg['ip']) + async def eventListener(self): s = self.ctx.socket(zmq.SUB) s.bind(self.config['events']['listen_address']) - logger.info("Listen for events on: {}".format(self.config['events']['listen_address'])) - + logger.info("Listen for events on: {}".format( + self.config['events']['listen_address'])) + for id in self.hugvey_ids: s.subscribe(getTopic(id)) - + while self.isRunning.is_set(): - hugvey_id, msg = await zmqReceive(s) - - if hugvey_id not in self.hugvey_ids: - logger.critical("Message from alien Hugvey: {}".format(hugvey_id)) - continue - elif hugvey_id not in self.hugveys: - if msg['event'] == 'connection': - # Create a hugvey - await self.instantiateHugvey(hugvey_id, msg) + try: + hugvey_id, msg = await zmqReceive(s) + + if hugvey_id not in self.hugvey_ids: + logger.critical( + "Message from alien Hugvey: {}".format(hugvey_id)) + continue + elif hugvey_id not in self.hugveys: + if msg['event'] == 'connection': + # Create a hugvey + await self.instantiateHugvey(hugvey_id, msg) + else: + logger.warning( + "Message from uninstantiated Hugvey {}".format(hugvey_id)) + logger.debug("Message contains: {}".format(msg)) + continue else: - logger.warning("Message from uninstantiated Hugvey {}".format(hugvey_id)) - logger.debug("Message contains: {}".format(msg)) - continue - else: - await self.hugveys[hugvey_id].eventQueue.put(msg) - pass - -# def getPanopticon(self): -# self.panopticon = - + await self.hugveys[hugvey_id].eventQueue.put(msg) + except Exception as e: + logger.critical(f"Exception while running event loop:") + logger.exception(e) + + def start(self): self.isRunning.set() self.loop = asyncio.get_event_loop() # self.panopticon_loop = asyncio.new_event_loop() - - self.tasks = {} # collect tasks so we can cancel in case of error - self.tasks['eventListener'] = self.loop.create_task(self.eventListener()) - self.tasks['commandSender'] = self.loop.create_task(self.commandSender()) - - print(threading.current_thread()) + + self.tasks = {} # collect tasks so we can cancel in case of error + self.tasks['eventListener'] = self.loop.create_task( + self.eventListener()) + self.tasks['commandSender'] = self.loop.create_task( + self.commandSender()) + # we want the web interface in a separate thread - self.panopticon_thread = threading.Thread(target=self.panopticon.start, name="Panopticon") + self.panopticon_thread = threading.Thread( + target=self.panopticon.start, name="Panopticon") self.panopticon_thread.start() - print(threading.current_thread()) self.loop.run_forever() - + def stop(self): self.isRunning.clear() - class HugveyState(object): """Represents the state of a Hugvey client on the server. Manages server connections & voice parsing etc. """ + def __init__(self, id: int, command: CentralCommand): + self.id = id self.command = command self.logger = logging.getLogger(f"hugvey{self.id}") self.loop = asyncio.new_event_loop() self.isConfigured = False + self.isRunning = threading.Event() self.eventQueue = None self.language_code = 'en-GB' self.story = Story(self) self.story.setStoryData(self.command.languages[self.language_code]) - + def config(self, hostname, ip): self.ip = ip self.hostname = hostname - self.logger.info(f"Hugvey {self.id} at {self.ip}, host: {self.hostname}") - + self.logger.info( + f"Hugvey {self.id} at {self.ip}, host: {self.hostname}") + if self.isConfigured == True: # a reconfiguration/reconnection pass - + self.isConfigured = True - + def sendCommand(self, msg): """ Send message or command to hugvey @param msg: The message to be sent. Probably a dict() """ self.command.commandHugvey(self.id, msg) - + def start(self): """ Start the tasks """ - # stop on isRunning.is_set() or wait() -# self.loop.create_task(self.processAudio()) tasks = asyncio.gather( self.catchException(self.processAudio()), self.catchException(self.handleEvents()), self.catchException(self.playStory()), loop=self.loop) self.loop.run_until_complete(tasks) -# asyncio.run_coroutine_threadsafe(self._start(), self.loop) + self.isRunning.set() async def catchException(self, awaitable): try: @@ -228,7 +287,7 @@ class HugveyState(object): except Exception as e: logger.exception(e) logger.critical(f"Hugvey restart required but not implemented yet") - + # TODO: restart def queueEvent(self, msg): @@ -243,51 +302,74 @@ class HugveyState(object): self.story.events.append(msg) async def handleEvents(self): - self.eventQueue = asyncio.Queue() # start event queue here, to avoid loop issues + self.eventQueue = asyncio.Queue() # start event queue here, to avoid loop issues while self.command.isRunning.is_set(): event = await self.eventQueue.get() self.logger.info("Received: {}".format(event)) - - if event['event'] =='language': + + if event['event'] == 'language': self.setLanguage(event['code']) + if event['event'] == 'pause': + self.pause() + if event['event'] == 'restart': + self.restart() + if event['event'] == 'resume': + self.resume() + self.eventQueue = None - + def setLanguage(self, language_code): if language_code not in self.command.languages: raise Exception("Invalid language {}".format(language_code)) - + self.language_code = language_code self.google.setLanguage(language_code) - + self.story.reset() self.story.setStoryData(self.command.languages[language_code]) + def pause(self): + self.google.pause() + self.story.pause() + self.isRunning.clear() + + def resume(self): + self.google.resume() + self.story.resume() + self.isRunning.set() + + def restart(self): + self.story.reset() + self.resume() + self.isRunning.set() + async def playStory(self): await self.story.start() - + async def processAudio(self): ''' Start the audio streamer service ''' self.logger.info("Start audio stream") streamer = AudioStreamer( - self.command.config['voice']['chunk'], + self.command.config['voice']['chunk'], self.ip, int(self.command.config['voice']['port'])) - + if self.command.debug: self.logger.warn("Debug on: Connecting Audio player") - self.player = Player(self.command.config['voice']['src_rate'], self.command.config['voice']['out_rate']) + self.player = Player( + self.command.config['voice']['src_rate'], self.command.config['voice']['out_rate']) streamer.addConsumer(self.player) - + self.logger.info("Start Speech") self.google = GoogleVoiceClient( hugvey=self, src_rate=self.command.config['voice']['src_rate'], credential_file=self.command.config['voice']['google_credentials'], language_code=self.language_code - ) + ) streamer.addConsumer(self.google) - + await streamer.run() diff --git a/hugvey/panopticon.py b/hugvey/panopticon.py index b33d824..adf8b23 100644 --- a/hugvey/panopticon.py +++ b/hugvey/panopticon.py @@ -11,28 +11,73 @@ import tornado.ioloop import os from pytz.reference import Central import asyncio +import json logger = logging.getLogger("panopticon") -web_dir = os.path.join(os.path.split(__file__)[0], '..','www') +web_dir = os.path.join(os.path.split(__file__)[0], '..', 'www') print(web_dir) -class WebSocketHandler(tornado.websocket.WebSocketHandler): - connections = set() - # the client connected - def open(self): - self.connections.add(self) - print("New client connected") +def getWebSocketHandler(central_command): + class WebSocketHandler(tornado.websocket.WebSocketHandler): + connections = set() - # the client sent the message - def on_message(self, message): - [con.write_message(message) for con in self.connections] + # the client connected + def open(self): + self.connections.add(self) + logger.info("New client connected") - # client disconnected - def on_close(self): - self.connections.remove(self) - print("Client disconnected") + # the client sent the message + def on_message(self, message): + try: + msg = json.loads(message) + if msg['action'] == 'init': + self.msgInit() + if msg['action'] == 'get_status': + self.msgStatus() + if msg['action'] == 'resume': + self.msgResume(msg['hugvey']) + if msg['action'] == 'pause': + self.msgPause(msg['hugvey']) + if msg['action'] == 'restart': + self.msgRestart(msg['hugvey']) + + except Exception as e: + self.send({'alert': 'Invalid request: {}'.format(e)}) + + def send(self, message): + j = json.dumps(message) + [con.write_message(j) for con in self.connections] + + # client disconnected + def on_close(self): + self.connections.remove(self) + logger.info("Client disconnected") + + def getStatusMsg(self): + msg = central_command.getStatusSummary() + msg['action'] = 'status' + + return msg + + def msgStatus(self): + self.send(self.getStatusMsg()) + + def msgInit(self): + msg = self.getStatusMsg() + self.send(msg) + + def msgResume(self, hv_id): + central_command.hugveys[hv_id].eventQueue.put({'event': 'resume'}) + + def msgPause(self, hv_id): + central_command.hugveys[hv_id].eventQueue.put({'event': 'pause'}) + + def msgRestart(self, hv_id): + central_command.hugveys[hv_id].eventQueue.put({'event': 'restart'}) + + return WebSocketHandler class Panopticon(object): @@ -40,22 +85,23 @@ class Panopticon(object): self.command = central_command self.config = config self.application = tornado.web.Application([ - (r"/ws", WebSocketHandler), - (r"/uploads/(.*)", tornado.web.StaticFileHandler, {"path": config['web']['files_dir']}), - (r"/(.*)", tornado.web.StaticFileHandler, {"path": web_dir, "default_filename": 'index.html'}), + (r"/ws", getWebSocketHandler(self.command)), + (r"/uploads/(.*)", tornado.web.StaticFileHandler, + {"path": config['web']['files_dir']}), + (r"/(.*)", tornado.web.StaticFileHandler, + {"path": web_dir, "default_filename": 'index.html'}), ], debug=True) - - + self.application.listen(config['web']['port']) # self.loop.configure(evt_loop) def start(self): evt_loop = asyncio.new_event_loop() asyncio.set_event_loop(evt_loop) - + self.loop = tornado.ioloop.IOLoop.current() logger.info(f"Start Panopticon on port {self.config['web']['port']}") self.loop.start() - + def stop(self): self.loop.stop() diff --git a/hugvey/story.py b/hugvey/story.py index d54f72d..a5dee2f 100644 --- a/hugvey/story.py +++ b/hugvey/story.py @@ -7,6 +7,7 @@ import asyncio logger = logging.getLogger("narrative") + class Message(object): def __init__(self, id, text): self.id = id @@ -16,9 +17,9 @@ class Message(object): @classmethod def initFromJson(message, data, story): - msg = message(data['@id'], data['text']) + msg = message(data['@id'], data['text']) msg.isStart = data['start'] if 'start' in data else False - return msg; + return msg def setReply(self, text): self.reply = text @@ -28,7 +29,8 @@ class Message(object): def getReply(self): if self.reply is None: - raise Exception("Getting reply while there is none! {0}".format(self.id)) + raise Exception( + "Getting reply while there is none! {0}".format(self.id)) return self.reply @@ -38,6 +40,7 @@ class Condition(object): A condition, basic conditions are built in, custom condition can be given by providing a custom method. """ + def __init__(self, id): self.id = id self.method = None @@ -45,7 +48,7 @@ class Condition(object): @classmethod def initFromJson(conditionClass, data, story): - condition = conditionClass(data['@id']) + condition = conditionClass(data['@id']) # TODO: should Condition be subclassed? if data['type'] == "replyContains": condition.method = condition._hasMetReplyContains @@ -55,7 +58,7 @@ class Condition(object): if 'vars' in data: condition.vars = data['vars'] - return condition; + return condition def isMet(self, story): """ @@ -98,6 +101,7 @@ class Direction(object): """ A condition based edge in the story graph """ + def __init__(self, id, msgFrom: Message, msgTo: Message): self.id = id self.msgFrom = msgFrom @@ -111,18 +115,19 @@ class Direction(object): def initFromJson(direction, data, story): msgFrom = story.get(data['source']) msgTo = story.get(data['target']) - direction = direction(data['@id'], msgFrom, msgTo) + direction = direction(data['@id'], msgFrom, msgTo) if 'conditions' in data: for conditionId in data['conditions']: c = story.get(conditionId) direction.addCondition(c) - return direction; + return direction class Interruption(object): """ An Interruption. Used to catch events outside of story flow. """ + def __init__(self, id): self.id = id self.conditions = [] @@ -132,12 +137,13 @@ class Interruption(object): @classmethod def initFromJson(interruptionClass, data, story): - interrupt = interruptionClass(data['@id']) + interrupt = interruptionClass(data['@id']) if 'conditions' in data: for conditionId in data['conditions']: c = story.get(conditionId) interrupt.addCondition(c) - return interrupt; + return interrupt + storyClasses = { 'Msg': Message, @@ -146,34 +152,103 @@ storyClasses = { 'Interruption': Interruption, } + +class Stopwatch(object): + """ + Keep track of elapsed time. Use multiple markers, but a single pause/resume button + """ + def __init__(self): + self.isRunning = asyncio.Event() + self.reset() + + def getElapsed(self, since_mark='start'): + t = time.time() + if self.paused_at != 0: + pause_duration = t - self.paused_at + else: + pause_duration = 0 + return t - self.marks[since_mark] - pause_duration + + def pause(self): + self.paused_at = time.time() + self.isRunning.clear() + + def resume(self): + if self.paused_at == 0: + return + + pause_duration = time.time() - self.paused_at + for m in self.marks: + self.marks[m] += pause_duration + + self.paused_at = 0 + self.isRunning.set() + + def reset(self): + self.marks = {} + self.setMark('start') + self.paused_at = 0 + self.isRunning.set() + + def setMark(self, name): + self.marks[name] = time.time() + + + def clearMark(self, name): + if name in self.marks: + self.marks.pop(name) + class Story(object): """Story represents and manages a story/narrative flow""" - #TODO should we separate 'narrative' (the graph) from the story (the current user flow) + # TODO should we separate 'narrative' (the graph) from the story (the + # current user flow) + def __init__(self, hugvey_state): super(Story, self).__init__() self.hugvey = hugvey_state - - self.events = [] # queue of received events - self.commands = [] # queue of commands to send - self.log = [] # all nodes/elements that are triggered - self.currentMessage = None + self.events = [] # queue of received events + self.commands = [] # queue of commands to send + self.log = [] # all nodes/elements that are triggered + self.currentMessage = None + self.timer = Stopwatch() + + def pause(self): + logger.debug('pause hugvey') + self.timer.pause() + + def resume(self): + logger.debug('resume hugvey') + self.timer.resume() + + def getStoryCounts(self): +# counts = {} +# for item in self.log: +# n =item.__class__.__name__ +# if n not in counts: +# counts[n] = 0 +# counts[n] += 1 +# return counts + return { + 'messages': len([e for e in self.log if isinstance(e, Message)]), + 'interruptions': len([e for e in self.log if isinstance(e, Interruption)]) + } def setStoryData(self, story_data): """ Parse self.data into a working story engine """ self.data = story_data - + # keep to be able to reset it in the end currentId = self.currentMessage.id if self.currentMessage else None - + self.elements = {} self.interruptions = [] self.directionsPerMsg = {} - self.startMessage = None # The entrypoint to the graph + self.startMessage = None # The entrypoint to the graph self.reset() - + for el in self.data: className = storyClasses[el['@type']] obj = className.initFromJson(el, self) @@ -181,22 +256,31 @@ class Story(object): logger.debug(self.elements) logger.debug(self.directionsPerMsg) - + if currentId: self.currentMessage = self.get(currentId) if self.currentMessage: - logger.info(f"Reinstantiated current message: {self.currentMessage.id}") + logger.info( + f"Reinstantiated current message: {self.currentMessage.id}") else: - logger.warn("Could not reinstatiate current message. Starting over") + logger.warn( + "Could not reinstatiate current message. Starting over") def reset(self): - self.startTime = time.time() - self.currentMessage = None # currently active message, determines active listeners etc. + self.timer.reset() +# self.startTime = time.time() + # currently active message, determines active listeners etc. + self.currentMessage = None self.lastMsgTime = None self.lastSpeechStartTime = None self.lastSpeechEndTime = None - self.variables = {} # captured variables from replies + self.variables = {} # captured variables from replies + self.finish_time = False + self.events = [] # queue of received events + self.commands = [] # queue of commands to send + self.log = [] # all nodes/elements that are triggered + def add(self, obj): if obj.id in self.elements: # print(obj) @@ -223,25 +307,24 @@ class Story(object): return self.elements[id] return None - def stop(self): logger.info("Stop Story") if self.isRunning: self.isRunning = False - def _processPendingEvents(self): # Gather events: nr = len(self.events) for i in range(nr): e = self.events.pop(0) - logger.info("handle '{}'".format( e )) + logger.info("handle '{}'".format(e)) if e['event'] == "exit": self.stop() if e['event'] == 'connect': # a client connected. Shold only happen in the beginning or in case of error # that is, until we have a 'reset' or 'start' event. - self.setCurrentMessage(self.currentMessage) # reinitiate current message + # reinitiate current message + self.setCurrentMessage(self.currentMessage) if e['event'] == "playbackFinish": if e['msgId'] == self.currentMessage.id: @@ -254,6 +337,7 @@ class Story(object): if e['event'] == 'speech': # log if somebody starts speaking + # TODO: use pausing timer if self.lastSpeechStartTime is None or self.lastSpeechStartTime < self.lastMsgTime: self.lastSpeechStartTime = e['time'] @@ -266,7 +350,8 @@ class Story(object): for direction in directions: for condition in direction.conditions: if condition.isMet(self): - logger.info("Condition is met: {0}, going to {1}".format(condition.id, direction.msgTo.id)) + logger.info("Condition is met: {0}, going to {1}".format( + condition.id, direction.msgTo.id)) self.log.append(condition) self.log.append(direction) self.setCurrentMessage(direction.msgTo) @@ -276,20 +361,21 @@ class Story(object): """ every 1/10 sec. determine what needs to be done based on the current story state """ - loopDuration = 0.1 # Configure fps + loopDuration = 0.1 # Configure fps lastTime = time.time() logger.info("Start renderer") while self.isRunning: if self.isRunning is False: break + + # pause on timer paused + await self.timer.isRunning.wait() # wait for un-pause for i in range(len(self.events)): self._processPendingEvents() if self.currentMessage.id not in self.directionsPerMsg: - # TODO: finish! - - pass + self.finish() directions = self.getCurrentDirections() self._processDirections(directions) @@ -307,9 +393,11 @@ class Story(object): def setCurrentMessage(self, message): self.currentMessage = message self.lastMsgTime = time.time() - self.lastMsgFinishTime = None # to be filled in by the event + self.lastMsgFinishTime = None # to be filled in by the event - logger.info("Current message: ({0}) \"{1}\"".format(message.id, message.text)) + logger.info("Current message: ({0}) \"{1}\"".format( + message.id, message.text)) + self.log.append(message) # TODO: prep events & timer etc. self.hugvey.sendCommand({ 'action': 'play', @@ -321,8 +409,8 @@ class Story(object): for direction in self.getCurrentDirections(): conditions = [c.id for c in direction.conditions] - logger.debug("- {0} -> {1} (when: {2}) ".format(direction.msgFrom.id, direction.msgTo.id, conditions)) - + logger.debug( + "- {0} -> {1} (when: {2}) ".format(direction.msgFrom.id, direction.msgTo.id, conditions)) def getCurrentDirections(self): if self.currentMessage.id not in self.directionsPerMsg: @@ -332,10 +420,21 @@ class Story(object): async def start(self): logger.info("Starting story") - self.startTime = time.time() + self.timer.reset() +# self.startTime = time.time() self.isRunning = True self.setCurrentMessage(self.startMessage) await self._renderer() - - + def isFinished(self): + if hasattr(self, 'finish_time'): + return self.finish_time + + return False + + def finish(self): + logger.info(f"Finished story for {self.hugvey.id}") + self.hugvey.pause() + self.finish_time = time.time() + self.timer.pause() + diff --git a/hugvey/voice/google.py b/hugvey/voice/google.py index 1e50ee7..21003f5 100644 --- a/hugvey/voice/google.py +++ b/hugvey/voice/google.py @@ -32,17 +32,26 @@ class GoogleVoiceClient(object): # Create a thread-safe buffer of audio data self.buffer = queue.Queue() - self.isRunning = False + self.isRunning = threading.Event() + self.toBeShutdown = False self.target_rate = 16000 self.cv_laststate = None self.restart = False + self.task = threading.Thread(target=self.run, name=f"hugvey#{self.hugvey.id}v") self.task.setDaemon(True) self.task.start() + + def pause(self): + self.isRunning.clear() + self.restart = True + + def resume(self): + self.isRunning.set() def generator(self): - while self.isRunning: + while not self.toBeShutdown: yield self.buffer.get() def setLanguage(self, language_code): @@ -54,11 +63,13 @@ class GoogleVoiceClient(object): self.restart = True def run(self): - self.isRunning = True + self.isRunning.set() - while self.isRunning: + while not self.toBeShutdown: try: + self.isRunning.wait() + self.speech_client = speech.SpeechClient() config = types.RecognitionConfig( encoding=enums.RecognitionConfig.AudioEncoding.LINEAR16, @@ -122,7 +133,7 @@ class GoogleVoiceClient(object): self.restart = False raise RequireRestart("Restart required") - if not self.isRunning: + if self.toBeShutdown: logger.warn("Stopping voice loop") break except RequireRestart as e: @@ -142,7 +153,7 @@ class GoogleVoiceClient(object): self.buffer.put_nowait(data) def shutdown(self): - self.isRunning = False + self.toBeShutdown = True diff --git a/local/.gitignore b/local/.gitignore new file mode 100644 index 0000000..a5baada --- /dev/null +++ b/local/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore + diff --git a/server_config.yml b/server_config.yml index 63e77db..b8cc1d0 100644 --- a/server_config.yml +++ b/server_config.yml @@ -7,7 +7,7 @@ voice: port: 4444 chunk: 2972 google_credentials: "/home/ruben/Documents/Projecten/2018/Hugvey/test_googlespeech/My First Project-0c7833e0d5fa.json" -hugveys: 3 +hugveys: 25 languages: - code: en-GB file: story_en.json diff --git a/www/hugvey_console.js b/www/hugvey_console.js new file mode 100644 index 0000000..436e11c --- /dev/null +++ b/www/hugvey_console.js @@ -0,0 +1,100 @@ +var panopticon; + +class Panopticon { + constructor() { + console.log( "Init panopticon" ); + this.hugveys = new Vue( { + el: "#status", + data: { + uptime: 0, + languages: [], + hugveys: [] + }, + methods: { + time_passed: function (hugvey, property) { + console.log("property!", Date(hugvey[property] * 1000)); + return moment(Date(hugvey[property] * 1000)).fromNow(); + } + } + } ); + + + this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: true, reconnectInterval: 3000 } ); + + + this.socket.addEventListener( 'open', ( e ) => { + this.send( { action: 'init' } ); + } ); + + this.socket.addEventListener( 'close', function( e ) { + console.log( 'Closed connection' ); + } ); + this.socket.addEventListener( 'message', ( e ) => { + let msg = JSON.parse( e.data ); + if ( typeof msg['alert'] !== 'undefined' ) { + alert(msg['alert']); + } + + if ( typeof msg['action'] === 'undefined' ) { + console.error( "not a valid message: " + e.data ); + return; + } + + switch ( msg['action'] ) { + + case 'status': + this.hugveys.uptime = this.stringToHHMMSS(msg['uptime']); + this.hugveys.languages = msg['languages']; + this.hugveys.hugveys = msg['hugveys']; + break; + } + } ); + } + + send( msg ) { + this.socket.send( JSON.stringify( msg ) ); + } + + getStatus() { +// console.log('get status', this, panopticon); + panopticon.send( { action: 'get_status' } ); + } + + init() { + setInterval( this.getStatus, 3000 ); + } + + stringToHHMMSS (string) { + var sec_num = parseInt(string, 10); // don't forget the second param + var hours = Math.floor(sec_num / 3600); + var minutes = Math.floor((sec_num - (hours * 3600)) / 60); + var seconds = sec_num - (hours * 3600) - (minutes * 60); + + if (hours < 10) {hours = "0"+hours;} + if (minutes < 10) {minutes = "0"+minutes;} + if (seconds < 10) {seconds = "0"+seconds;} + return hours+':'+minutes+':'+seconds; + } + + + loadNarrative(code, file) { + + } + + resume(hv_id) { + this.send({ action: 'resume', hugvey: hv_id }) + } + pause(hv_id) { + this.send({ action: 'play', hugvey: hv_id }) + } + restart(hv_id) { + this.send({ action: 'restart', hugvey: hv_id }) + } +} + + + +window.addEventListener( 'load', function() { + panopticon = new Panopticon(); + panopticon.init(); +}) \ No newline at end of file diff --git a/www/index.html b/www/index.html index 3f40d66..0734a71 100644 --- a/www/index.html +++ b/www/index.html @@ -1,17 +1,43 @@ - Pillow Talk Control Interface +Pillow Talk Control Interface + + + + -
-
-
-
-
-
-
{{message}}
- +
+
+
+
Uptime
+
{{uptime}}
+
Languages
+
{{lang.code}}
+
+
+
+

+ {{ hv.id }} + +

+
+ {{ hv.language }} / {{ hv.msg }} +
+ Finished: {{time_passed(hv, 'finished')}} +
+
{{key}}
{{c}}
+
Pause
+
Resume
+
+
+
+
+
+ \ No newline at end of file diff --git a/www/moment.min.js b/www/moment.min.js new file mode 100644 index 0000000..2a3358f --- /dev/null +++ b/www/moment.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.moment=t()}(this,function(){"use strict";var e,i;function c(){return e.apply(null,arguments)}function o(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function u(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function l(e){return void 0===e}function d(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function h(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function f(e,t){var n,s=[];for(n=0;n>>0,s=0;sDe(e)?(r=e+1,o-De(e)):(r=e,o),{year:r,dayOfYear:a}}function Ie(e,t,n){var s,i,r=Ve(e.year(),t,n),a=Math.floor((e.dayOfYear()-r-1)/7)+1;return a<1?s=a+Ae(i=e.year()-1,t,n):a>Ae(e.year(),t,n)?(s=a-Ae(e.year(),t,n),i=e.year()+1):(i=e.year(),s=a),{week:s,year:i}}function Ae(e,t,n){var s=Ve(e,t,n),i=Ve(e+1,t,n);return(De(e)-s+i)/7}I("w",["ww",2],"wo","week"),I("W",["WW",2],"Wo","isoWeek"),H("week","w"),H("isoWeek","W"),L("week",5),L("isoWeek",5),ue("w",B),ue("ww",B,z),ue("W",B),ue("WW",B,z),fe(["w","ww","W","WW"],function(e,t,n,s){t[s.substr(0,1)]=k(e)});I("d",0,"do","day"),I("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),I("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),I("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),I("e",0,0,"weekday"),I("E",0,0,"isoWeekday"),H("day","d"),H("weekday","e"),H("isoWeekday","E"),L("day",11),L("weekday",11),L("isoWeekday",11),ue("d",B),ue("e",B),ue("E",B),ue("dd",function(e,t){return t.weekdaysMinRegex(e)}),ue("ddd",function(e,t){return t.weekdaysShortRegex(e)}),ue("dddd",function(e,t){return t.weekdaysRegex(e)}),fe(["dd","ddd","dddd"],function(e,t,n,s){var i=n._locale.weekdaysParse(e,s,n._strict);null!=i?t.d=i:g(n).invalidWeekday=e}),fe(["d","e","E"],function(e,t,n,s){t[s]=k(e)});var je="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var Ze="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var ze="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var $e=ae;var qe=ae;var Je=ae;function Be(){function e(e,t){return t.length-e.length}var t,n,s,i,r,a=[],o=[],u=[],l=[];for(t=0;t<7;t++)n=y([2e3,1]).day(t),s=this.weekdaysMin(n,""),i=this.weekdaysShort(n,""),r=this.weekdays(n,""),a.push(s),o.push(i),u.push(r),l.push(s),l.push(i),l.push(r);for(a.sort(e),o.sort(e),u.sort(e),l.sort(e),t=0;t<7;t++)o[t]=de(o[t]),u[t]=de(u[t]),l[t]=de(l[t]);this._weekdaysRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+a.join("|")+")","i")}function Qe(){return this.hours()%12||12}function Xe(e,t){I(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)})}function Ke(e,t){return t._meridiemParse}I("H",["HH",2],0,"hour"),I("h",["hh",2],0,Qe),I("k",["kk",2],0,function(){return this.hours()||24}),I("hmm",0,0,function(){return""+Qe.apply(this)+U(this.minutes(),2)}),I("hmmss",0,0,function(){return""+Qe.apply(this)+U(this.minutes(),2)+U(this.seconds(),2)}),I("Hmm",0,0,function(){return""+this.hours()+U(this.minutes(),2)}),I("Hmmss",0,0,function(){return""+this.hours()+U(this.minutes(),2)+U(this.seconds(),2)}),Xe("a",!0),Xe("A",!1),H("hour","h"),L("hour",13),ue("a",Ke),ue("A",Ke),ue("H",B),ue("h",B),ue("k",B),ue("HH",B,z),ue("hh",B,z),ue("kk",B,z),ue("hmm",Q),ue("hmmss",X),ue("Hmm",Q),ue("Hmmss",X),ce(["H","HH"],ge),ce(["k","kk"],function(e,t,n){var s=k(e);t[ge]=24===s?0:s}),ce(["a","A"],function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e}),ce(["h","hh"],function(e,t,n){t[ge]=k(e),g(n).bigHour=!0}),ce("hmm",function(e,t,n){var s=e.length-2;t[ge]=k(e.substr(0,s)),t[pe]=k(e.substr(s)),g(n).bigHour=!0}),ce("hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[ge]=k(e.substr(0,s)),t[pe]=k(e.substr(s,2)),t[ve]=k(e.substr(i)),g(n).bigHour=!0}),ce("Hmm",function(e,t,n){var s=e.length-2;t[ge]=k(e.substr(0,s)),t[pe]=k(e.substr(s))}),ce("Hmmss",function(e,t,n){var s=e.length-4,i=e.length-2;t[ge]=k(e.substr(0,s)),t[pe]=k(e.substr(s,2)),t[ve]=k(e.substr(i))});var et,tt=Te("Hours",!0),nt={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:He,monthsShort:Re,week:{dow:0,doy:6},weekdays:je,weekdaysMin:ze,weekdaysShort:Ze,meridiemParse:/[ap]\.?m?\.?/i},st={},it={};function rt(e){return e?e.toLowerCase().replace("_","-"):e}function at(e){var t=null;if(!st[e]&&"undefined"!=typeof module&&module&&module.exports)try{t=et._abbr,require("./locale/"+e),ot(t)}catch(e){}return st[e]}function ot(e,t){var n;return e&&((n=l(t)?lt(e):ut(e,t))?et=n:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),et._abbr}function ut(e,t){if(null===t)return delete st[e],null;var n,s=nt;if(t.abbr=e,null!=st[e])T("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=st[e]._config;else if(null!=t.parentLocale)if(null!=st[t.parentLocale])s=st[t.parentLocale]._config;else{if(null==(n=at(t.parentLocale)))return it[t.parentLocale]||(it[t.parentLocale]=[]),it[t.parentLocale].push({name:e,config:t}),null;s=n._config}return st[e]=new P(b(s,t)),it[e]&&it[e].forEach(function(e){ut(e.name,e.config)}),ot(e),st[e]}function lt(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return et;if(!o(e)){if(t=at(e))return t;e=[e]}return function(e){for(var t,n,s,i,r=0;r=t&&a(i,n,!0)>=t-1)break;t--}r++}return et}(e)}function dt(e){var t,n=e._a;return n&&-2===g(e).overflow&&(t=n[_e]<0||11Pe(n[me],n[_e])?ye:n[ge]<0||24Ae(n,r,a)?g(e)._overflowWeeks=!0:null!=u?g(e)._overflowWeekday=!0:(o=Ee(n,s,i,r,a),e._a[me]=o.year,e._dayOfYear=o.dayOfYear)}(e),null!=e._dayOfYear&&(r=ht(e._a[me],s[me]),(e._dayOfYear>De(r)||0===e._dayOfYear)&&(g(e)._overflowDayOfYear=!0),n=Ge(r,0,e._dayOfYear),e._a[_e]=n.getUTCMonth(),e._a[ye]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=a[t]=s[t];for(;t<7;t++)e._a[t]=a[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[ge]&&0===e._a[pe]&&0===e._a[ve]&&0===e._a[we]&&(e._nextDay=!0,e._a[ge]=0),e._d=(e._useUTC?Ge:function(e,t,n,s,i,r,a){var o=new Date(e,t,n,s,i,r,a);return e<100&&0<=e&&isFinite(o.getFullYear())&&o.setFullYear(e),o}).apply(null,a),i=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[ge]=24),e._w&&void 0!==e._w.d&&e._w.d!==i&&(g(e).weekdayMismatch=!0)}}var ft=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,mt=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,_t=/Z|[+-]\d\d(?::?\d\d)?/,yt=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],gt=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],pt=/^\/?Date\((\-?\d+)/i;function vt(e){var t,n,s,i,r,a,o=e._i,u=ft.exec(o)||mt.exec(o);if(u){for(g(e).iso=!0,t=0,n=yt.length;tn.valueOf():n.valueOf()this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},ln.isLocal=function(){return!!this.isValid()&&!this._isUTC},ln.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},ln.isUtc=Vt,ln.isUTC=Vt,ln.zoneAbbr=function(){return this._isUTC?"UTC":""},ln.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},ln.dates=n("dates accessor is deprecated. Use date instead.",nn),ln.months=n("months accessor is deprecated. Use month instead",Fe),ln.years=n("years accessor is deprecated. Use year instead",Oe),ln.zone=n("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,t){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,t),this):-this.utcOffset()}),ln.isDSTShifted=n("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!l(this._isDSTShifted))return this._isDSTShifted;var e={};if(w(e,this),(e=Yt(e))._a){var t=e._isUTC?y(e._a):Tt(e._a);this._isDSTShifted=this.isValid()&&0 + + + + Pillow Talk - Narrative Builder + + + + + +
+

Hugvey

+

Select a narrative json file

+ +
+ + +
+
+
+
+
Save JSON
+
New Message
+
+ + + + + + + + + + diff --git a/www/reconnecting-websocket.js b/www/reconnecting-websocket.js new file mode 100644 index 0000000..0cd4332 --- /dev/null +++ b/www/reconnecting-websocket.js @@ -0,0 +1,365 @@ +// MIT License: +// +// Copyright (c) 2010-2012, Joe Walnes +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +/** + * This behaves like a WebSocket in every way, except if it fails to connect, + * or it gets disconnected, it will repeatedly poll until it successfully connects + * again. + * + * It is API compatible, so when you have: + * ws = new WebSocket('ws://....'); + * you can replace with: + * ws = new ReconnectingWebSocket('ws://....'); + * + * The event stream will typically look like: + * onconnecting + * onopen + * onmessage + * onmessage + * onclose // lost connection + * onconnecting + * onopen // sometime later... + * onmessage + * onmessage + * etc... + * + * It is API compatible with the standard WebSocket API, apart from the following members: + * + * - `bufferedAmount` + * - `extensions` + * - `binaryType` + * + * Latest version: https://github.com/joewalnes/reconnecting-websocket/ + * - Joe Walnes + * + * Syntax + * ====== + * var socket = new ReconnectingWebSocket(url, protocols, options); + * + * Parameters + * ========== + * url - The url you are connecting to. + * protocols - Optional string or array of protocols. + * options - See below + * + * Options + * ======= + * Options can either be passed upon instantiation or set after instantiation: + * + * var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 }); + * + * or + * + * var socket = new ReconnectingWebSocket(url); + * socket.debug = true; + * socket.reconnectInterval = 4000; + * + * debug + * - Whether this instance should log debug messages. Accepts true or false. Default: false. + * + * automaticOpen + * - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close(). + * + * reconnectInterval + * - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000. + * + * maxReconnectInterval + * - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000. + * + * reconnectDecay + * - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5. + * + * timeoutInterval + * - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000. + * + */ +(function (global, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof module !== 'undefined' && module.exports){ + module.exports = factory(); + } else { + global.ReconnectingWebSocket = factory(); + } +})(this, function () { + + if (!('WebSocket' in window)) { + return; + } + + function ReconnectingWebSocket(url, protocols, options) { + + // Default settings + var settings = { + + /** Whether this instance should log debug messages. */ + debug: false, + + /** Whether or not the websocket should attempt to connect immediately upon instantiation. */ + automaticOpen: true, + + /** The number of milliseconds to delay before attempting to reconnect. */ + reconnectInterval: 1000, + /** The maximum number of milliseconds to delay a reconnection attempt. */ + maxReconnectInterval: 30000, + /** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */ + reconnectDecay: 1.5, + + /** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */ + timeoutInterval: 2000, + + /** The maximum number of reconnection attempts to make. Unlimited if null. */ + maxReconnectAttempts: null, + + /** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */ + binaryType: 'blob' + } + if (!options) { options = {}; } + + // Overwrite and define settings with options if they exist. + for (var key in settings) { + if (typeof options[key] !== 'undefined') { + this[key] = options[key]; + } else { + this[key] = settings[key]; + } + } + + // These should be treated as read-only properties + + /** The URL as resolved by the constructor. This is always an absolute URL. Read only. */ + this.url = url; + + /** The number of attempted reconnects since starting, or the last successful connection. Read only. */ + this.reconnectAttempts = 0; + + /** + * The current state of the connection. + * Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED + * Read only. + */ + this.readyState = WebSocket.CONNECTING; + + /** + * A string indicating the name of the sub-protocol the server selected; this will be one of + * the strings specified in the protocols parameter when creating the WebSocket object. + * Read only. + */ + this.protocol = null; + + // Private state variables + + var self = this; + var ws; + var forcedClose = false; + var timedOut = false; + var eventTarget = document.createElement('div'); + + // Wire up "on*" properties as event handlers + + eventTarget.addEventListener('open', function(event) { self.onopen(event); }); + eventTarget.addEventListener('close', function(event) { self.onclose(event); }); + eventTarget.addEventListener('connecting', function(event) { self.onconnecting(event); }); + eventTarget.addEventListener('message', function(event) { self.onmessage(event); }); + eventTarget.addEventListener('error', function(event) { self.onerror(event); }); + + // Expose the API required by EventTarget + + this.addEventListener = eventTarget.addEventListener.bind(eventTarget); + this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget); + this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget); + + /** + * This function generates an event that is compatible with standard + * compliant browsers and IE9 - IE11 + * + * This will prevent the error: + * Object doesn't support this action + * + * http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563 + * @param s String The name that the event should use + * @param args Object an optional object that the event will use + */ + function generateEvent(s, args) { + var evt = document.createEvent("CustomEvent"); + evt.initCustomEvent(s, false, false, args); + return evt; + }; + + this.open = function (reconnectAttempt) { + ws = new WebSocket(self.url, protocols || []); + ws.binaryType = this.binaryType; + + if (reconnectAttempt) { + if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) { + return; + } + } else { + eventTarget.dispatchEvent(generateEvent('connecting')); + this.reconnectAttempts = 0; + } + + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'attempt-connect', self.url); + } + + var localWs = ws; + var timeout = setTimeout(function() { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'connection-timeout', self.url); + } + timedOut = true; + localWs.close(); + timedOut = false; + }, self.timeoutInterval); + + ws.onopen = function(event) { + clearTimeout(timeout); + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onopen', self.url); + } + self.protocol = ws.protocol; + self.readyState = WebSocket.OPEN; + self.reconnectAttempts = 0; + var e = generateEvent('open'); + e.isReconnect = reconnectAttempt; + reconnectAttempt = false; + eventTarget.dispatchEvent(e); + }; + + ws.onclose = function(event) { + clearTimeout(timeout); + ws = null; + if (forcedClose) { + self.readyState = WebSocket.CLOSED; + eventTarget.dispatchEvent(generateEvent('close')); + } else { + self.readyState = WebSocket.CONNECTING; + var e = generateEvent('connecting'); + e.code = event.code; + e.reason = event.reason; + e.wasClean = event.wasClean; + eventTarget.dispatchEvent(e); + if (!reconnectAttempt && !timedOut) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onclose', self.url); + } + eventTarget.dispatchEvent(generateEvent('close')); + } + + var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts); + setTimeout(function() { + self.reconnectAttempts++; + self.open(true); + }, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout); + } + }; + ws.onmessage = function(event) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data); + } + var e = generateEvent('message'); + e.data = event.data; + eventTarget.dispatchEvent(e); + }; + ws.onerror = function(event) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'onerror', self.url, event); + } + eventTarget.dispatchEvent(generateEvent('error')); + }; + } + + // Whether or not to create a websocket upon instantiation + if (this.automaticOpen == true) { + this.open(false); + } + + /** + * Transmits data to the server over the WebSocket connection. + * + * @param data a text string, ArrayBuffer or Blob to send to the server. + */ + this.send = function(data) { + if (ws) { + if (self.debug || ReconnectingWebSocket.debugAll) { + console.debug('ReconnectingWebSocket', 'send', self.url, data); + } + return ws.send(data); + } else { + throw 'INVALID_STATE_ERR : Pausing to reconnect websocket'; + } + }; + + /** + * Closes the WebSocket connection or connection attempt, if any. + * If the connection is already CLOSED, this method does nothing. + */ + this.close = function(code, reason) { + // Default CLOSE_NORMAL code + if (typeof code == 'undefined') { + code = 1000; + } + forcedClose = true; + if (ws) { + ws.close(code, reason); + } + }; + + /** + * Additional public API method to refresh the connection if still open (close, re-open). + * For example, if the app suspects bad data / missed heart beats, it can try to refresh. + */ + this.refresh = function() { + if (ws) { + ws.close(); + } + }; + } + + /** + * An event listener to be called when the WebSocket connection's readyState changes to OPEN; + * this indicates that the connection is ready to send and receive data. + */ + ReconnectingWebSocket.prototype.onopen = function(event) {}; + /** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */ + ReconnectingWebSocket.prototype.onclose = function(event) {}; + /** An event listener to be called when a connection begins being attempted. */ + ReconnectingWebSocket.prototype.onconnecting = function(event) {}; + /** An event listener to be called when a message is received from the server. */ + ReconnectingWebSocket.prototype.onmessage = function(event) {}; + /** An event listener to be called when an error occurs. */ + ReconnectingWebSocket.prototype.onerror = function(event) {}; + + /** + * Whether all instances of ReconnectingWebSocket should log debug messages. + * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true. + */ + ReconnectingWebSocket.debugAll = false; + + ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING; + ReconnectingWebSocket.OPEN = WebSocket.OPEN; + ReconnectingWebSocket.CLOSING = WebSocket.CLOSING; + ReconnectingWebSocket.CLOSED = WebSocket.CLOSED; + + return ReconnectingWebSocket; +}); diff --git a/www/styles.css b/www/styles.css new file mode 100644 index 0000000..797a45b --- /dev/null +++ b/www/styles.css @@ -0,0 +1,44 @@ +body{ + font-family: sans-serif; +} + +#status{ + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 430px; + height: 100%; + overflow-y: scroll; +} +#status > div{ + width: 33.3333333%; + height: 150px; + border: solid 1px; + box-sizing: border-box; + position: relative; +} + +.hugvey{ + background-image: linear-gradient(to top, #587457, #35a589); + color:white; + display: flex; + flex-direction: column; + justify-content: center; +} +.hugvey.hugvey--off{ + background-image: linear-gradient(to top, #575d74, #3572a5); +} + +.hugvey h1{ + text-align: center; + margin: 0; +} +.hugvey.hugvey--off h1::after{ + content: '[off]' +} +.hugvey.hugvey--on h1 { + position: absolute; + left: 5px; + top: 5px; + +} \ No newline at end of file diff --git a/www/vue.js b/www/vue.js new file mode 100644 index 0000000..7ae4d30 --- /dev/null +++ b/www/vue.js @@ -0,0 +1,11055 @@ +/*! + * Vue.js v2.5.22 + * (c) 2014-2019 Evan You + * Released under the MIT License. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.Vue = factory()); +}(this, (function () { 'use strict'; + + /* */ + + var emptyObject = Object.freeze({}); + + // These helpers produce better VM code in JS engines due to their + // explicitness and function inlining. + function isUndef (v) { + return v === undefined || v === null + } + + function isDef (v) { + return v !== undefined && v !== null + } + + function isTrue (v) { + return v === true + } + + function isFalse (v) { + return v === false + } + + /** + * Check if value is primitive. + */ + function isPrimitive (value) { + return ( + typeof value === 'string' || + typeof value === 'number' || + // $flow-disable-line + typeof value === 'symbol' || + typeof value === 'boolean' + ) + } + + /** + * Quick object check - this is primarily used to tell + * Objects from primitive values when we know the value + * is a JSON-compliant type. + */ + function isObject (obj) { + return obj !== null && typeof obj === 'object' + } + + /** + * Get the raw type string of a value, e.g., [object Object]. + */ + var _toString = Object.prototype.toString; + + function toRawType (value) { + return _toString.call(value).slice(8, -1) + } + + /** + * Strict object type check. Only returns true + * for plain JavaScript objects. + */ + function isPlainObject (obj) { + return _toString.call(obj) === '[object Object]' + } + + function isRegExp (v) { + return _toString.call(v) === '[object RegExp]' + } + + /** + * Check if val is a valid array index. + */ + function isValidArrayIndex (val) { + var n = parseFloat(String(val)); + return n >= 0 && Math.floor(n) === n && isFinite(val) + } + + /** + * Convert a value to a string that is actually rendered. + */ + function toString (val) { + return val == null + ? '' + : typeof val === 'object' + ? JSON.stringify(val, null, 2) + : String(val) + } + + /** + * Convert an input value to a number for persistence. + * If the conversion fails, return original string. + */ + function toNumber (val) { + var n = parseFloat(val); + return isNaN(n) ? val : n + } + + /** + * Make a map and return a function for checking if a key + * is in that map. + */ + function makeMap ( + str, + expectsLowerCase + ) { + var map = Object.create(null); + var list = str.split(','); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase + ? function (val) { return map[val.toLowerCase()]; } + : function (val) { return map[val]; } + } + + /** + * Check if a tag is a built-in tag. + */ + var isBuiltInTag = makeMap('slot,component', true); + + /** + * Check if an attribute is a reserved attribute. + */ + var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + + /** + * Remove an item from an array. + */ + function remove (arr, item) { + if (arr.length) { + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1) + } + } + } + + /** + * Check whether an object has the property. + */ + var hasOwnProperty = Object.prototype.hasOwnProperty; + function hasOwn (obj, key) { + return hasOwnProperty.call(obj, key) + } + + /** + * Create a cached version of a pure function. + */ + function cached (fn) { + var cache = Object.create(null); + return (function cachedFn (str) { + var hit = cache[str]; + return hit || (cache[str] = fn(str)) + }) + } + + /** + * Camelize a hyphen-delimited string. + */ + var camelizeRE = /-(\w)/g; + var camelize = cached(function (str) { + return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) + }); + + /** + * Capitalize a string. + */ + var capitalize = cached(function (str) { + return str.charAt(0).toUpperCase() + str.slice(1) + }); + + /** + * Hyphenate a camelCase string. + */ + var hyphenateRE = /\B([A-Z])/g; + var hyphenate = cached(function (str) { + return str.replace(hyphenateRE, '-$1').toLowerCase() + }); + + /** + * Simple bind polyfill for environments that do not support it, + * e.g., PhantomJS 1.x. Technically, we don't need this anymore + * since native bind is now performant enough in most browsers. + * But removing it would mean breaking code that was able to run in + * PhantomJS 1.x, so this must be kept for backward compatibility. + */ + + /* istanbul ignore next */ + function polyfillBind (fn, ctx) { + function boundFn (a) { + var l = arguments.length; + return l + ? l > 1 + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) + : fn.call(ctx) + } + + boundFn._length = fn.length; + return boundFn + } + + function nativeBind (fn, ctx) { + return fn.bind(ctx) + } + + var bind = Function.prototype.bind + ? nativeBind + : polyfillBind; + + /** + * Convert an Array-like object to a real Array. + */ + function toArray (list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret + } + + /** + * Mix properties into target object. + */ + function extend (to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to + } + + /** + * Merge an Array of Objects into a single Object. + */ + function toObject (arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res + } + + /* eslint-disable no-unused-vars */ + + /** + * Perform no operation. + * Stubbing args to make Flow happy without leaving useless transpiled code + * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/). + */ + function noop (a, b, c) {} + + /** + * Always return false. + */ + var no = function (a, b, c) { return false; }; + + /* eslint-enable no-unused-vars */ + + /** + * Return the same value. + */ + var identity = function (_) { return _; }; + + /** + * Generate a string containing static keys from compiler modules. + */ + function genStaticKeys (modules) { + return modules.reduce(function (keys, m) { + return keys.concat(m.staticKeys || []) + }, []).join(',') + } + + /** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + */ + function looseEqual (a, b) { + if (a === b) { return true } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]) + }) + } else if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime() + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]) + }) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } + } + + /** + * Return the first index at which a loosely equal value can be + * found in the array (if value is a plain object, the array must + * contain an object of the same shape), or -1 if it is not present. + */ + function looseIndexOf (arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) { return i } + } + return -1 + } + + /** + * Ensure a function is called only once. + */ + function once (fn) { + var called = false; + return function () { + if (!called) { + called = true; + fn.apply(this, arguments); + } + } + } + + var SSR_ATTR = 'data-server-rendered'; + + var ASSET_TYPES = [ + 'component', + 'directive', + 'filter' + ]; + + var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured' + ]; + + /* */ + + + + var config = ({ + /** + * Option merge strategies (used in core/util/options) + */ + // $flow-disable-line + optionMergeStrategies: Object.create(null), + + /** + * Whether to suppress warnings. + */ + silent: false, + + /** + * Show production mode tip message on boot? + */ + productionTip: "development" !== 'production', + + /** + * Whether to enable devtools + */ + devtools: "development" !== 'production', + + /** + * Whether to record perf + */ + performance: false, + + /** + * Error handler for watcher errors + */ + errorHandler: null, + + /** + * Warn handler for watcher warns + */ + warnHandler: null, + + /** + * Ignore certain custom elements + */ + ignoredElements: [], + + /** + * Custom user key aliases for v-on + */ + // $flow-disable-line + keyCodes: Object.create(null), + + /** + * Check if a tag is reserved so that it cannot be registered as a + * component. This is platform-dependent and may be overwritten. + */ + isReservedTag: no, + + /** + * Check if an attribute is reserved so that it cannot be used as a component + * prop. This is platform-dependent and may be overwritten. + */ + isReservedAttr: no, + + /** + * Check if a tag is an unknown element. + * Platform-dependent. + */ + isUnknownElement: no, + + /** + * Get the namespace of an element + */ + getTagNamespace: noop, + + /** + * Parse the real tag name for the specific platform. + */ + parsePlatformTagName: identity, + + /** + * Check if an attribute must be bound using property, e.g. value + * Platform-dependent. + */ + mustUseProp: no, + + /** + * Perform updates asynchronously. Intended to be used by Vue Test Utils + * This will significantly reduce performance if set to false. + */ + async: true, + + /** + * Exposed for legacy reasons + */ + _lifecycleHooks: LIFECYCLE_HOOKS + }); + + /* */ + + /** + * Check if a string starts with $ or _ + */ + function isReserved (str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5F + } + + /** + * Define a property. + */ + function def (obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + + /** + * Parse simple path. + */ + var bailRE = /[^\w.$]/; + function parsePath (path) { + if (bailRE.test(path)) { + return + } + var segments = path.split('.'); + return function (obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) { return } + obj = obj[segments[i]]; + } + return obj + } + } + + /* */ + + // can we use __proto__? + var hasProto = '__proto__' in {}; + + // Browser environment sniffing + var inBrowser = typeof window !== 'undefined'; + var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; + var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); + var isIE = UA && /msie|trident/.test(UA); + var isIE9 = UA && UA.indexOf('msie 9.0') > 0; + var isEdge = UA && UA.indexOf('edge/') > 0; + var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android'); + var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); + var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; + + // Firefox has a "watch" function on Object.prototype... + var nativeWatch = ({}).watch; + + var supportsPassive = false; + if (inBrowser) { + try { + var opts = {}; + Object.defineProperty(opts, 'passive', ({ + get: function get () { + /* istanbul ignore next */ + supportsPassive = true; + } + })); // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts); + } catch (e) {} + } + + // this needs to be lazy-evaled because vue may be required before + // vue-server-renderer can set VUE_ENV + var _isServer; + var isServerRendering = function () { + if (_isServer === undefined) { + /* istanbul ignore if */ + if (!inBrowser && !inWeex && typeof global !== 'undefined') { + // detect presence of vue-server-renderer and avoid + // Webpack shimming the process + _isServer = global['process'] && global['process'].env.VUE_ENV === 'server'; + } else { + _isServer = false; + } + } + return _isServer + }; + + // detect devtools + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + + /* istanbul ignore next */ + function isNative (Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) + } + + var hasSymbol = + typeof Symbol !== 'undefined' && isNative(Symbol) && + typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); + + var _Set; + /* istanbul ignore if */ // $flow-disable-line + if (typeof Set !== 'undefined' && isNative(Set)) { + // use native Set when available. + _Set = Set; + } else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = /*@__PURE__*/(function () { + function Set () { + this.set = Object.create(null); + } + Set.prototype.has = function has (key) { + return this.set[key] === true + }; + Set.prototype.add = function add (key) { + this.set[key] = true; + }; + Set.prototype.clear = function clear () { + this.set = Object.create(null); + }; + + return Set; + }()); + } + + /* */ + + var warn = noop; + var tip = noop; + var generateComponentTrace = (noop); // work around flow check + var formatComponentName = (noop); + + { + var hasConsole = typeof console !== 'undefined'; + var classifyRE = /(?:^|[-_])(\w)/g; + var classify = function (str) { return str + .replace(classifyRE, function (c) { return c.toUpperCase(); }) + .replace(/[-_]/g, ''); }; + + warn = function (msg, vm) { + var trace = vm ? generateComponentTrace(vm) : ''; + + if (config.warnHandler) { + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && (!config.silent)) { + console.error(("[Vue warn]: " + msg + trace)); + } + }; + + tip = function (msg, vm) { + if (hasConsole && (!config.silent)) { + console.warn("[Vue tip]: " + msg + ( + vm ? generateComponentTrace(vm) : '' + )); + } + }; + + formatComponentName = function (vm, includeFile) { + if (vm.$root === vm) { + return '' + } + var options = typeof vm === 'function' && vm.cid != null + ? vm.options + : vm._isVue + ? vm.$options || vm.constructor.options + : vm; + var name = options.name || options._componentTag; + var file = options.__file; + if (!name && file) { + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + + return ( + (name ? ("<" + (classify(name)) + ">") : "") + + (file && includeFile !== false ? (" at " + file) : '') + ) + }; + + var repeat = function (str, n) { + var res = ''; + while (n) { + if (n % 2 === 1) { res += str; } + if (n > 1) { str += str; } + n >>= 1; + } + return res + }; + + generateComponentTrace = function (vm) { + if (vm._isVue && vm.$parent) { + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + currentRecursiveSequence++; + vm = vm.$parent; + continue + } else if (currentRecursiveSequence > 0) { + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + tree.push(vm); + vm = vm.$parent; + } + return '\n\nfound in\n\n' + tree + .map(function (vm, i) { return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) + ? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + : formatComponentName(vm))); }) + .join('\n') + } else { + return ("\n\n(found in " + (formatComponentName(vm)) + ")") + } + }; + } + + /* */ + + var uid = 0; + + /** + * A dep is an observable that can have multiple + * directives subscribing to it. + */ + var Dep = function Dep () { + this.id = uid++; + this.subs = []; + }; + + Dep.prototype.addSub = function addSub (sub) { + this.subs.push(sub); + }; + + Dep.prototype.removeSub = function removeSub (sub) { + remove(this.subs, sub); + }; + + Dep.prototype.depend = function depend () { + if (Dep.target) { + Dep.target.addDep(this); + } + }; + + Dep.prototype.notify = function notify () { + // stabilize the subscriber list first + var subs = this.subs.slice(); + if (!config.async) { + // subs aren't sorted in scheduler if not running async + // we need to sort them now to make sure they fire in correct + // order + subs.sort(function (a, b) { return a.id - b.id; }); + } + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } + }; + + // The current target watcher being evaluated. + // This is globally unique because only one watcher + // can be evaluated at a time. + Dep.target = null; + var targetStack = []; + + function pushTarget (target) { + targetStack.push(target); + Dep.target = target; + } + + function popTarget () { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; + } + + /* */ + + var VNode = function VNode ( + tag, + data, + children, + text, + elm, + context, + componentOptions, + asyncFactory + ) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; + this.context = context; + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = undefined; + this.parent = undefined; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; + }; + + var prototypeAccessors = { child: { configurable: true } }; + + // DEPRECATED: alias for componentInstance for backwards compat. + /* istanbul ignore next */ + prototypeAccessors.child.get = function () { + return this.componentInstance + }; + + Object.defineProperties( VNode.prototype, prototypeAccessors ); + + var createEmptyVNode = function (text) { + if ( text === void 0 ) text = ''; + + var node = new VNode(); + node.text = text; + node.isComment = true; + return node + }; + + function createTextVNode (val) { + return new VNode(undefined, undefined, undefined, String(val)) + } + + // optimized shallow clone + // used for static nodes and slot nodes because they may be reused across + // multiple renders, cloning them avoids errors when DOM manipulations rely + // on their elm reference. + function cloneVNode (vnode) { + var cloned = new VNode( + vnode.tag, + vnode.data, + // #7975 + // clone children array to avoid mutating original in case of cloning + // a child. + vnode.children && vnode.children.slice(), + vnode.text, + vnode.elm, + vnode.context, + vnode.componentOptions, + vnode.asyncFactory + ); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.asyncMeta = vnode.asyncMeta; + cloned.isCloned = true; + return cloned + } + + /* + * not type checking this file because flow doesn't play well with + * dynamically accessing methods on Array prototype + */ + + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto); + + var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' + ]; + + /** + * Intercept mutating methods and emit events + */ + methodsToPatch.forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + inserted = args; + break + case 'splice': + inserted = args.slice(2); + break + } + if (inserted) { ob.observeArray(inserted); } + // notify change + ob.dep.notify(); + return result + }); + }); + + /* */ + + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + + /** + * In some cases we may want to disable observation inside a component's + * update computation. + */ + var shouldObserve = true; + + function toggleObserving (value) { + shouldObserve = value; + } + + /** + * Observer class that is attached to each observed + * object. Once attached, the observer converts the target + * object's property keys into getter/setters that + * collect dependencies and dispatch updates. + */ + var Observer = function Observer (value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + def(value, '__ob__', this); + if (Array.isArray(value)) { + if (hasProto) { + protoAugment(value, arrayMethods); + } else { + copyAugment(value, arrayMethods, arrayKeys); + } + this.observeArray(value); + } else { + this.walk(value); + } + }; + + /** + * Walk through all properties and convert them into + * getter/setters. This method should only be called when + * value type is Object. + */ + Observer.prototype.walk = function walk (obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive$$1(obj, keys[i]); + } + }; + + /** + * Observe a list of Array items. + */ + Observer.prototype.observeArray = function observeArray (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } + }; + + // helpers + + /** + * Augment a target Object or Array by intercepting + * the prototype chain using __proto__ + */ + function protoAugment (target, src) { + /* eslint-disable no-proto */ + target.__proto__ = src; + /* eslint-enable no-proto */ + } + + /** + * Augment a target Object or Array by defining + * hidden properties. + */ + /* istanbul ignore next */ + function copyAugment (target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } + } + + /** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + */ + function observe (value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + return + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if ( + shouldObserve && + !isServerRendering() && + (Array.isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value._isVue + ) { + ob = new Observer(value); + } + if (asRootData && ob) { + ob.vmCount++; + } + return ob + } + + /** + * Define a reactive property on an Object. + */ + function defineReactive$$1 ( + obj, + key, + val, + customSetter, + shallow + ) { + var dep = new Dep(); + + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + var getter = property && property.get; + var setter = property && property.set; + if ((!getter || setter) && arguments.length === 2) { + val = obj[key]; + } + + var childOb = !shallow && observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter () { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } + } + } + return value + }, + set: function reactiveSetter (newVal) { + var value = getter ? getter.call(obj) : val; + /* eslint-disable no-self-compare */ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare */ + if (customSetter) { + customSetter(); + } + // #7981: for accessor properties without setter + if (getter && !setter) { return } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = !shallow && observe(newVal); + dep.notify(); + } + }); + } + + /** + * Set a property on an object. Adds the new property and + * triggers change notification if the property doesn't + * already exist. + */ + function set (target, key, val) { + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + return val + } + if (key in target && !(key in Object.prototype)) { + target[key] = val; + return val + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.' + ); + return val + } + if (!ob) { + target[key] = val; + return val + } + defineReactive$$1(ob.value, key, val); + ob.dep.notify(); + return val + } + + /** + * Delete a property and trigger change if necessary. + */ + function del (target, key) { + if (isUndef(target) || isPrimitive(target) + ) { + warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target)))); + } + if (Array.isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1); + return + } + var ob = (target).__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn( + 'Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.' + ); + return + } + if (!hasOwn(target, key)) { + return + } + delete target[key]; + if (!ob) { + return + } + ob.dep.notify(); + } + + /** + * Collect dependencies on array elements when the array is touched, since + * we cannot intercept array element access like property getters. + */ + function dependArray (value) { + for (var e = (void 0), i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + dependArray(e); + } + } + } + + /* */ + + /** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + */ + var strats = config.optionMergeStrategies; + + /** + * Options with restrictions + */ + { + strats.el = strats.propsData = function (parent, child, vm, key) { + if (!vm) { + warn( + "option \"" + key + "\" can only be used during instance " + + 'creation with the `new` keyword.' + ); + } + return defaultStrat(parent, child) + }; + } + + /** + * Helper that recursively merges two data objects together. + */ + function mergeData (to, from) { + if (!from) { return to } + var key, toVal, fromVal; + var keys = Object.keys(from); + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if ( + toVal !== fromVal && + isPlainObject(toVal) && + isPlainObject(fromVal) + ) { + mergeData(toVal, fromVal); + } + } + return to + } + + /** + * Data + */ + function mergeDataOrFn ( + parentVal, + childVal, + vm + ) { + if (!vm) { + // in a Vue.extend merge, both should be functions + if (!childVal) { + return parentVal + } + if (!parentVal) { + return childVal + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + return function mergedDataFn () { + return mergeData( + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ) + } + } else { + return function mergedInstanceDataFn () { + // instance merge + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + if (instanceData) { + return mergeData(instanceData, defaultData) + } else { + return defaultData + } + } + } + } + + strats.data = function ( + parentVal, + childVal, + vm + ) { + if (!vm) { + if (childVal && typeof childVal !== 'function') { + warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); + + return parentVal + } + return mergeDataOrFn(parentVal, childVal) + } + + return mergeDataOrFn(parentVal, childVal, vm) + }; + + /** + * Hooks and props are merged as arrays. + */ + function mergeHook ( + parentVal, + childVal + ) { + var res = childVal + ? parentVal + ? parentVal.concat(childVal) + : Array.isArray(childVal) + ? childVal + : [childVal] + : parentVal; + return res + ? dedupeHooks(res) + : res + } + + function dedupeHooks (hooks) { + var res = []; + for (var i = 0; i < hooks.length; i++) { + if (res.indexOf(hooks[i]) === -1) { + res.push(hooks[i]); + } + } + return res + } + + LIFECYCLE_HOOKS.forEach(function (hook) { + strats[hook] = mergeHook; + }); + + /** + * Assets + * + * When a vm is present (instance creation), we need to do + * a three-way merge between constructor options, instance + * options and parent options. + */ + function mergeAssets ( + parentVal, + childVal, + vm, + key + ) { + var res = Object.create(parentVal || null); + if (childVal) { + assertObjectType(key, childVal, vm); + return extend(res, childVal) + } else { + return res + } + } + + ASSET_TYPES.forEach(function (type) { + strats[type + 's'] = mergeAssets; + }); + + /** + * Watchers. + * + * Watchers hashes should not overwrite one + * another, so we merge them as arrays. + */ + strats.watch = function ( + parentVal, + childVal, + vm, + key + ) { + // work around Firefox's Object.prototype.watch... + if (parentVal === nativeWatch) { parentVal = undefined; } + if (childVal === nativeWatch) { childVal = undefined; } + /* istanbul ignore if */ + if (!childVal) { return Object.create(parentVal || null) } + { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = {}; + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + ret[key$1] = parent + ? parent.concat(child) + : Array.isArray(child) ? child : [child]; + } + return ret + }; + + /** + * Other object hashes. + */ + strats.props = + strats.methods = + strats.inject = + strats.computed = function ( + parentVal, + childVal, + vm, + key + ) { + if (childVal && "development" !== 'production') { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { return childVal } + var ret = Object.create(null); + extend(ret, parentVal); + if (childVal) { extend(ret, childVal); } + return ret + }; + strats.provide = mergeDataOrFn; + + /** + * Default strategy. + */ + var defaultStrat = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal + }; + + /** + * Validate component names + */ + function checkComponents (options) { + for (var key in options.components) { + validateComponentName(key); + } + } + + function validateComponentName (name) { + if (!/^[a-zA-Z][\w-]*$/.test(name)) { + warn( + 'Invalid component name: "' + name + '". Component names ' + + 'can only contain alphanumeric characters and the hyphen, ' + + 'and must start with a letter.' + ); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn( + 'Do not use built-in or reserved HTML elements as component ' + + 'id: ' + name + ); + } + } + + /** + * Ensure all props option syntax are normalized into the + * Object-based format. + */ + function normalizeProps (options, vm) { + var props = options.props; + if (!props) { return } + var res = {}; + var i, val, name; + if (Array.isArray(props)) { + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + name = camelize(val); + res[name] = { type: null }; + } else { + warn('props must be strings when using array syntax.'); + } + } + } else if (isPlainObject(props)) { + for (var key in props) { + val = props[key]; + name = camelize(key); + res[name] = isPlainObject(val) + ? val + : { type: val }; + } + } else { + warn( + "Invalid value for option \"props\": expected an Array or an Object, " + + "but got " + (toRawType(props)) + ".", + vm + ); + } + options.props = res; + } + + /** + * Normalize all injections into Object-based format + */ + function normalizeInject (options, vm) { + var inject = options.inject; + if (!inject) { return } + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + for (var i = 0; i < inject.length; i++) { + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + for (var key in inject) { + var val = inject[key]; + normalized[key] = isPlainObject(val) + ? extend({ from: key }, val) + : { from: val }; + } + } else { + warn( + "Invalid value for option \"inject\": expected an Array or an Object, " + + "but got " + (toRawType(inject)) + ".", + vm + ); + } + } + + /** + * Normalize raw function directives into object format. + */ + function normalizeDirectives (options) { + var dirs = options.directives; + if (dirs) { + for (var key in dirs) { + var def = dirs[key]; + if (typeof def === 'function') { + dirs[key] = { bind: def, update: def }; + } + } + } + } + + function assertObjectType (name, value, vm) { + if (!isPlainObject(value)) { + warn( + "Invalid value for option \"" + name + "\": expected an Object, " + + "but got " + (toRawType(value)) + ".", + vm + ); + } + } + + /** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + */ + function mergeOptions ( + parent, + child, + vm + ) { + { + checkComponents(child); + } + + if (typeof child === 'function') { + child = child.options; + } + + normalizeProps(child, vm); + normalizeInject(child, vm); + normalizeDirectives(child); + + // Apply extends and mixins on the child options, + // but only if it is a raw options object that isn't + // the result of another mergeOptions call. + // Only merged options has the _base property. + if (!child._base) { + if (child.extends) { + parent = mergeOptions(parent, child.extends, vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + } + + var options = {}; + var key; + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField (key) { + var strat = strats[key] || defaultStrat; + options[key] = strat(parent[key], child[key], vm, key); + } + return options + } + + /** + * Resolve an asset. + * This function is used because child instances need access + * to assets defined in its ancestor chain. + */ + function resolveAsset ( + options, + type, + id, + warnMissing + ) { + /* istanbul ignore if */ + if (typeof id !== 'string') { + return + } + var assets = options[type]; + // check local registration variations first + if (hasOwn(assets, id)) { return assets[id] } + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { return assets[camelizedId] } + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] } + // fallback to prototype chain + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if (warnMissing && !res) { + warn( + 'Failed to resolve ' + type.slice(0, -1) + ': ' + id, + options + ); + } + return res + } + + /* */ + + + + function validateProp ( + key, + propOptions, + propsData, + vm + ) { + var prop = propOptions[key]; + var absent = !hasOwn(propsData, key); + var value = propsData[key]; + // boolean casting + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { + if (absent && !hasOwn(prop, 'default')) { + value = false; + } else if (value === '' || value === hyphenate(key)) { + // only cast empty string / same name to boolean if + // boolean has higher priority + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + // check default value + if (value === undefined) { + value = getPropDefaultValue(vm, prop, key); + // since the default value is a fresh copy, + // make sure to observe it. + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + { + assertProp(prop, key, value, vm, absent); + } + return value + } + + /** + * Get the default value of a prop. + */ + function getPropDefaultValue (vm, prop, key) { + // no default, return undefined + if (!hasOwn(prop, 'default')) { + return undefined + } + var def = prop.default; + // warn against non-factory defaults for Object & Array + if (isObject(def)) { + warn( + 'Invalid default value for prop "' + key + '": ' + + 'Props with type Object/Array must use a factory function ' + + 'to return the default value.', + vm + ); + } + // the raw prop value was also undefined from previous render, + // return previous default value to avoid unnecessary watcher trigger + if (vm && vm.$options.propsData && + vm.$options.propsData[key] === undefined && + vm._props[key] !== undefined + ) { + return vm._props[key] + } + // call factory function for non-Function types + // a value is Function if its prototype is function even across different execution context + return typeof def === 'function' && getType(prop.type) !== 'Function' + ? def.call(vm) + : def + } + + /** + * Assert whether a prop is valid. + */ + function assertProp ( + prop, + name, + value, + vm, + absent + ) { + if (prop.required && absent) { + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + if (value == null && !prop.required) { + return + } + var type = prop.type; + var valid = !type || type === true; + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + type = [type]; + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ''); + valid = assertedType.valid; + } + } + + if (!valid) { + warn( + getInvalidTypeMessage(name, value, expectedTypes), + vm + ); + return + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } + } + + var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + + function assertType (value, type) { + var valid; + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + var t = typeof value; + valid = t === expectedType.toLowerCase(); + // for primitive wrapper objects + if (!valid && t === 'object') { + valid = value instanceof type; + } + } else if (expectedType === 'Object') { + valid = isPlainObject(value); + } else if (expectedType === 'Array') { + valid = Array.isArray(value); + } else { + valid = value instanceof type; + } + return { + valid: valid, + expectedType: expectedType + } + } + + /** + * Use function string name to check built-in types, + * because a simple equality check will fail when running + * across different vms / iframes. + */ + function getType (fn) { + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : '' + } + + function isSameType (a, b) { + return getType(a) === getType(b) + } + + function getTypeIndex (type, expectedTypes) { + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1 + } + for (var i = 0, len = expectedTypes.length; i < len; i++) { + if (isSameType(expectedTypes[i], type)) { + return i + } + } + return -1 + } + + function getInvalidTypeMessage (name, value, expectedTypes) { + var message = "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')); + var expectedType = expectedTypes[0]; + var receivedType = toRawType(value); + var expectedValue = styleValue(value, expectedType); + var receivedValue = styleValue(value, receivedType); + // check if we need to specify expected value + if (expectedTypes.length === 1 && + isExplicable(expectedType) && + !isBoolean(expectedType, receivedType)) { + message += " with value " + expectedValue; + } + message += ", got " + receivedType + " "; + // check if we need to specify received value + if (isExplicable(receivedType)) { + message += "with value " + receivedValue + "."; + } + return message + } + + function styleValue (value, type) { + if (type === 'String') { + return ("\"" + value + "\"") + } else if (type === 'Number') { + return ("" + (Number(value))) + } else { + return ("" + value) + } + } + + function isExplicable (value) { + var explicitTypes = ['string', 'number', 'boolean']; + return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; }) + } + + function isBoolean () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + + return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; }) + } + + /* */ + + function handleError (err, vm, info) { + if (vm) { + var cur = vm; + while ((cur = cur.$parent)) { + var hooks = cur.$options.errorCaptured; + if (hooks) { + for (var i = 0; i < hooks.length; i++) { + try { + var capture = hooks[i].call(cur, err, vm, info) === false; + if (capture) { return } + } catch (e) { + globalHandleError(e, cur, 'errorCaptured hook'); + } + } + } + } + } + globalHandleError(err, vm, info); + } + + function globalHandleError (err, vm, info) { + if (config.errorHandler) { + try { + return config.errorHandler.call(null, err, vm, info) + } catch (e) { + logError(e, null, 'config.errorHandler'); + } + } + logError(err, vm, info); + } + + function logError (err, vm, info) { + { + warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm); + } + /* istanbul ignore else */ + if ((inBrowser || inWeex) && typeof console !== 'undefined') { + console.error(err); + } else { + throw err + } + } + + /* */ + + var callbacks = []; + var pending = false; + + function flushCallbacks () { + pending = false; + var copies = callbacks.slice(0); + callbacks.length = 0; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + + // Here we have async deferring wrappers using both microtasks and (macro) tasks. + // In < 2.4 we used microtasks everywhere, but there are some scenarios where + // microtasks have too high a priority and fire in between supposedly + // sequential events (e.g. #4521, #6690) or even between bubbling of the same + // event (#6566). However, using (macro) tasks everywhere also has subtle problems + // when state is changed right before repaint (e.g. #6813, out-in transitions). + // Here we use microtask by default, but expose a way to force (macro) task when + // needed (e.g. in event handlers attached by v-on). + var microTimerFunc; + var macroTimerFunc; + var useMacroTask = false; + + // Determine (macro) task defer implementation. + // Technically setImmediate should be the ideal choice, but it's only available + // in IE. The only polyfill that consistently queues the callback after all DOM + // events triggered in the same loop is by using MessageChannel. + /* istanbul ignore if */ + if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { + macroTimerFunc = function () { + setImmediate(flushCallbacks); + }; + } else if (typeof MessageChannel !== 'undefined' && ( + isNative(MessageChannel) || + // PhantomJS + MessageChannel.toString() === '[object MessageChannelConstructor]' + )) { + var channel = new MessageChannel(); + var port = channel.port2; + channel.port1.onmessage = flushCallbacks; + macroTimerFunc = function () { + port.postMessage(1); + }; + } else { + /* istanbul ignore next */ + macroTimerFunc = function () { + setTimeout(flushCallbacks, 0); + }; + } + + // Determine microtask defer implementation. + /* istanbul ignore next, $flow-disable-line */ + if (typeof Promise !== 'undefined' && isNative(Promise)) { + var p = Promise.resolve(); + microTimerFunc = function () { + p.then(flushCallbacks); + // in problematic UIWebViews, Promise.then doesn't completely break, but + // it can get stuck in a weird state where callbacks are pushed into the + // microtask queue but the queue isn't being flushed, until the browser + // needs to do some other work, e.g. handle a timer. Therefore we can + // "force" the microtask queue to be flushed by adding an empty timer. + if (isIOS) { setTimeout(noop); } + }; + } else { + // fallback to macro + microTimerFunc = macroTimerFunc; + } + + /** + * Wrap a function so that if any code inside triggers state change, + * the changes are queued using a (macro) task instead of a microtask. + */ + function withMacroTask (fn) { + return fn._withTask || (fn._withTask = function () { + useMacroTask = true; + try { + return fn.apply(null, arguments) + } finally { + useMacroTask = false; + } + }) + } + + function nextTick (cb, ctx) { + var _resolve; + callbacks.push(function () { + if (cb) { + try { + cb.call(ctx); + } catch (e) { + handleError(e, ctx, 'nextTick'); + } + } else if (_resolve) { + _resolve(ctx); + } + }); + if (!pending) { + pending = true; + if (useMacroTask) { + macroTimerFunc(); + } else { + microTimerFunc(); + } + } + // $flow-disable-line + if (!cb && typeof Promise !== 'undefined') { + return new Promise(function (resolve) { + _resolve = resolve; + }) + } + } + + /* */ + + var mark; + var measure; + + { + var perf = inBrowser && window.performance; + /* istanbul ignore if */ + if ( + perf && + perf.mark && + perf.measure && + perf.clearMarks && + perf.clearMeasures + ) { + mark = function (tag) { return perf.mark(tag); }; + measure = function (name, startTag, endTag) { + perf.measure(name, startTag, endTag); + perf.clearMarks(startTag); + perf.clearMarks(endTag); + perf.clearMeasures(name); + }; + } + } + + /* not type checking this file because flow doesn't play well with Proxy */ + + var initProxy; + + { + var allowedGlobals = makeMap( + 'Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + + 'require' // for Webpack/Browserify + ); + + var warnNonPresent = function (target, key) { + warn( + "Property or method \"" + key + "\" is not defined on the instance but " + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', + target + ); + }; + + var warnReservedPrefix = function (target, key) { + warn( + "Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " + + 'properties starting with "$" or "_" are not proxied in the Vue instance to ' + + 'prevent conflicts with Vue internals' + + 'See: https://vuejs.org/v2/api/#data', + target + ); + }; + + var hasProxy = + typeof Proxy !== 'undefined' && isNative(Proxy); + + if (hasProxy) { + var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + config.keyCodes = new Proxy(config.keyCodes, { + set: function set (target, key, value) { + if (isBuiltInModifier(key)) { + warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key)); + return false + } else { + target[key] = value; + return true + } + } + }); + } + + var hasHandler = { + has: function has (target, key) { + var has = key in target; + var isAllowed = allowedGlobals(key) || + (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); + if (!has && !isAllowed) { + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + return has || !isAllowed + } + }; + + var getHandler = { + get: function get (target, key) { + if (typeof key === 'string' && !(key in target)) { + if (key in target.$data) { warnReservedPrefix(target, key); } + else { warnNonPresent(target, key); } + } + return target[key] + } + }; + + initProxy = function initProxy (vm) { + if (hasProxy) { + // determine which proxy handler to use + var options = vm.$options; + var handlers = options.render && options.render._withStripped + ? getHandler + : hasHandler; + vm._renderProxy = new Proxy(vm, handlers); + } else { + vm._renderProxy = vm; + } + }; + } + + /* */ + + var seenObjects = new _Set(); + + /** + * Recursively traverse an object to evoke all converted + * getters, so that every nested property inside the object + * is collected as a "deep" dependency. + */ + function traverse (val) { + _traverse(val, seenObjects); + seenObjects.clear(); + } + + function _traverse (val, seen) { + var i, keys; + var isA = Array.isArray(val); + if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { + return + } + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return + } + seen.add(depId); + } + if (isA) { + i = val.length; + while (i--) { _traverse(val[i], seen); } + } else { + keys = Object.keys(val); + i = keys.length; + while (i--) { _traverse(val[keys[i]], seen); } + } + } + + /* */ + + var normalizeEvent = cached(function (name) { + var passive = name.charAt(0) === '&'; + name = passive ? name.slice(1) : name; + var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first + name = once$$1 ? name.slice(1) : name; + var capture = name.charAt(0) === '!'; + name = capture ? name.slice(1) : name; + return { + name: name, + once: once$$1, + capture: capture, + passive: passive + } + }); + + function createFnInvoker (fns) { + function invoker () { + var arguments$1 = arguments; + + var fns = invoker.fns; + if (Array.isArray(fns)) { + var cloned = fns.slice(); + for (var i = 0; i < cloned.length; i++) { + cloned[i].apply(null, arguments$1); + } + } else { + // return handler return value for single handlers + return fns.apply(null, arguments) + } + } + invoker.fns = fns; + return invoker + } + + function updateListeners ( + on, + oldOn, + add, + remove$$1, + createOnceHandler, + vm + ) { + var name, def$$1, cur, old, event; + for (name in on) { + def$$1 = cur = on[name]; + old = oldOn[name]; + event = normalizeEvent(name); + if (isUndef(cur)) { + warn( + "Invalid handler for event \"" + (event.name) + "\": got " + String(cur), + vm + ); + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur); + } + if (isTrue(event.once)) { + cur = on[name] = createOnceHandler(event.name, cur, event.capture); + } + add(event.name, cur, event.capture, event.passive, event.params); + } else if (cur !== old) { + old.fns = cur; + on[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name); + remove$$1(event.name, oldOn[name], event.capture); + } + } + } + + /* */ + + function mergeVNodeHook (def, hookKey, hook) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + var oldHook = def[hookKey]; + + function wrappedHook () { + hook.apply(this, arguments); + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove(invoker.fns, wrappedHook); + } + + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]); + } else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook; + invoker.fns.push(wrappedHook); + } else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + + invoker.merged = true; + def[hookKey] = invoker; + } + + /* */ + + function extractPropsFromVNodeData ( + data, + Ctor, + tag + ) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return + } + var res = {}; + var attrs = data.attrs; + var props = data.props; + if (isDef(attrs) || isDef(props)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + { + var keyInLowerCase = key.toLowerCase(); + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + tip( + "Prop \"" + keyInLowerCase + "\" is passed to component " + + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + + " \"" + key + "\". " + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." + ); + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false); + } + } + return res + } + + function checkProp ( + res, + hash, + key, + altKey, + preserve + ) { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key]; + if (!preserve) { + delete hash[key]; + } + return true + } else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey]; + if (!preserve) { + delete hash[altKey]; + } + return true + } + } + return false + } + + /* */ + + // The template compiler attempts to minimize the need for normalization by + // statically analyzing the template at compile time. + // + // For plain HTML markup, normalization can be completely skipped because the + // generated render function is guaranteed to return Array. There are + // two cases where extra normalization is needed: + + // 1. When the children contains components - because a functional component + // may return an Array instead of a single root. In this case, just a simple + // normalization is needed - if any child is an Array, we flatten the whole + // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep + // because functional components already normalize their own children. + function simpleNormalizeChildren (children) { + for (var i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children) + } + } + return children + } + + // 2. When the children contains constructs that always generated nested Arrays, + // e.g.