Revision: 12760 http://trac.calendarserver.org//changeset/12760 Author: wsanchez@apple.com Date: 2014-02-26 18:34:09 -0800 (Wed, 26 Feb 2014) Log Message: ----------- New module for EventSource code. Added Paths: ----------- CalendarServer/trunk/calendarserver/webadmin/test/eventsource.py Added: CalendarServer/trunk/calendarserver/webadmin/test/eventsource.py =================================================================== --- CalendarServer/trunk/calendarserver/webadmin/test/eventsource.py (rev 0) +++ CalendarServer/trunk/calendarserver/webadmin/test/eventsource.py 2014-02-27 02:34:09 UTC (rev 12760) @@ -0,0 +1,224 @@ +# -*- test-case-name: calendarserver.webadmin.test.test_principals -*- +## +# Copyright (c) 2014 Apple Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from __future__ import print_function + +""" +Calendar Server principal management web UI. +""" + +__all__ = [ + "textAsEvent", + "EventSourceResource", +] + +from collections import deque + +from zope.interface import implementer, Interface + +from twisted.internet.defer import succeed + +from txweb2.stream import IByteStream, fallbackSplit +from txweb2.resource import Resource +from txweb2.http_headers import MimeType +from txweb2.http import Response + + +def textAsEvent(text, eventID=None, eventClass=None): + """ + Format some text as an HTML5 EventSource event. Since the EventSource data + format is text-oriented, this function expects L{unicode}, not L{bytes}; + binary data should be encoded as text if it is to be used in an EventSource + stream. + + @param text: The text (ie. the message) to send in the event. + @type text: L{unicode} + + @param eventID: An unique identifier for the event. + @type eventID: L{unicode} + + @param eventClass: A class name (ie. a categorization) for the event. + @type eventClass: L{unicode} + + @return: An HTML5 EventSource event as text. + @rtype: L{unicode} + """ + event = [] + + if eventID is not None: + event.append(u"id: {0}".format(eventID)) + + if eventClass is not None: + event.append(u"event: {0}".format(eventClass)) + + event.extend( + u"data: {0}".format(l) for l in text.split("\n") + ) + + return u"\n".join(event) + u"\n\n" + + + +class IEventDecoder(Interface): + """ + An object that can be used to extract data from an application-specific + event object for encoding into an EventSource data stream. + """ + + def idForEvent(event): + """ + @return: An unique identifier for the given event. + @rtype: L{unicode} + """ + + def classForEvent(event): + """ + @return: A class name (ie. a categorization) for the event. + @rtype: L{unicode} + """ + + def textForEvent(event): + """ + @return: The text (ie. the message) to send in the event. + @rtype: L{unicode} + """ + + + +class EventSourceResource(Resource): + """ + Resource that vends HTML5 EventSource events. + + Events are stored in a ring buffer and streamed to clients. + """ + + addSlash = False + + + def __init__(self, eventDecoder, bufferSize=400): + """ + @param eventDecoder: An object that can be used to extract data from + an event for encoding into an EventSource data stream. + @type eventDecoder: L{IEventDecoder} + + @param bufferSize: The maximum number of events to keep in the ring + buffer. + @type bufferSize: L{int} + """ + Resource.__init__(self) + + self._events = deque(maxlen=bufferSize) + + + def render(self, request): + lastID = request.headers.getRawHeaders(u"last-event-id") + + response = Response() + response.stream = EventStream(self._eventDecoder, self._events, lastID) + response.headers.setHeader( + b"content-type", MimeType.fromString(b"text/event-stream") + ) + return response + + + +@implementer(IByteStream) +class EventStream(object): + """ + L{IByteStream} that streams out HTML5 EventSource events. + """ + + length = None + + + def __init__(self, eventDecoder, events, lastID): + """ + @param eventDecoder: An object that can be used to extract data from + an event for encoding into an EventSource data stream. + @type eventDecoder: L{IEventDecoder} + + @param events: Application-specific event objects. + @type events: sequence of L{object} + + @param lastID: The identifier for the last event that was vended from + C{events}. Vending will resume starting from the following event. + @type lastID: L{int} + """ + object.__init__(self) + + self._eventDecoder = eventDecoder + self._events = events + self._lastID = lastID + self._closed = False + + + def addEvent(self, event): + self._events.append(event) + + + def read(self): + if self._closed: + return None + + lastID = self._lastID + eventID = None + + idForEvent = self.eventDecoder.idForEvent + classForEvent = self.eventDecoder.classForEvent + textForEvent = self.eventDecoder.textForEvent + + for event in self._events: + eventID = idForEvent(event) + + # If lastID is not None, skip messages up to and including the one + # referenced by lastID. + if lastID is not None: + if eventID == lastID: + eventID = None + lastID = None + + continue + + eventClass = classForEvent(event) + eventText = textForEvent(event) + + return succeed( + textAsEvent(eventText, eventID, eventClass).encode("utf-8") + ) + + + if eventID is not None: + # We just scanned all the messages, and none are the last one the + # client saw. + self._lastID = None + + return succeed("") + + return succeed(None) + + # from twisted.internet.task import deferLater + # from twisted.internet import reactor + + # return deferLater(reactor, 1.0, self.read) + + + def split(self, point): + return fallbackSplit(self, point) + + + def close(self): + self._closed = True
participants (1)
-
source_changes@macosforge.org