diff --git a/EventSource.xcodeproj/project.pbxproj b/EventSource.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1ac0a04 --- /dev/null +++ b/EventSource.xcodeproj/project.pbxproj @@ -0,0 +1,639 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 657A452F23433EDD0017B99A /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F83F230F38E9004C3141 /* Event.swift */; }; + 657A453023433EDD0017B99A /* EventSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F840230F38E9004C3141 /* EventSource.swift */; }; + 657A453123433EDD0017B99A /* EventStreamParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F841230F38E9004C3141 /* EventStreamParser.swift */; }; + E009F811230F387A004C3141 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F810230F387A004C3141 /* AppDelegate.swift */; }; + E009F818230F387B004C3141 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E009F817230F387B004C3141 /* Assets.xcassets */; }; + E009F81B230F387B004C3141 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E009F819230F387B004C3141 /* LaunchScreen.storyboard */; }; + E009F826230F387B004C3141 /* EventSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F825230F387B004C3141 /* EventSourceTests.swift */; }; + E009F842230F38E9004C3141 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F83F230F38E9004C3141 /* Event.swift */; }; + E009F843230F38E9004C3141 /* EventSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F840230F38E9004C3141 /* EventSource.swift */; }; + E009F844230F38E9004C3141 /* EventStreamParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F841230F38E9004C3141 /* EventStreamParser.swift */; }; + E009F856230F3948004C3141 /* EventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F855230F3948004C3141 /* EventTests.swift */; }; + E009F858230F3965004C3141 /* EventStreamParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F857230F3965004C3141 /* EventStreamParserTests.swift */; }; + E009F85F230F3AA6004C3141 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F85D230F3AA6004C3141 /* ViewController.swift */; }; + E009F860230F3AA6004C3141 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E009F85E230F3AA6004C3141 /* Main.storyboard */; }; + E0AC96D8231293D700DC89E4 /* URLSessionDataTaskMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0AC96D7231293D600DC89E4 /* URLSessionDataTaskMock.swift */; }; + E0AC96D92312A22200DC89E4 /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F83F230F38E9004C3141 /* Event.swift */; }; + E0AC96DA2312A22200DC89E4 /* EventSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F840230F38E9004C3141 /* EventSource.swift */; }; + E0AC96DB2312A22200DC89E4 /* EventStreamParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E009F841230F38E9004C3141 /* EventStreamParser.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + E009F822230F387B004C3141 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E009F805230F387A004C3141 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E009F80C230F387A004C3141; + remoteInfo = EventSource; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 657A452723433EBA0017B99A /* EventSource.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EventSource.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 657A452A23433EBA0017B99A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E009F80D230F387A004C3141 /* EventSource.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = EventSource.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E009F810230F387A004C3141 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + E009F817230F387B004C3141 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E009F81A230F387B004C3141 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + E009F81C230F387B004C3141 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E009F821230F387B004C3141 /* EventSourceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EventSourceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E009F825230F387B004C3141 /* EventSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventSourceTests.swift; sourceTree = ""; }; + E009F827230F387C004C3141 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E009F83F230F38E9004C3141 /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; + E009F840230F38E9004C3141 /* EventSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventSource.swift; sourceTree = ""; }; + E009F841230F38E9004C3141 /* EventStreamParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventStreamParser.swift; sourceTree = ""; }; + E009F855230F3948004C3141 /* EventTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventTests.swift; sourceTree = ""; }; + E009F857230F3965004C3141 /* EventStreamParserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventStreamParserTests.swift; sourceTree = ""; }; + E009F85D230F3AA6004C3141 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + E009F85E230F3AA6004C3141 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; + E0AC96D7231293D600DC89E4 /* URLSessionDataTaskMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataTaskMock.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 657A452423433EBA0017B99A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E009F80A230F387A004C3141 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E009F81E230F387B004C3141 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E009F804230F387A004C3141 = { + isa = PBXGroup; + children = ( + E009F83E230F38C0004C3141 /* EventSource */, + E009F80F230F387A004C3141 /* EventSourceSample */, + E009F824230F387B004C3141 /* EventSourceTests */, + E009F80E230F387A004C3141 /* Products */, + ); + sourceTree = ""; + }; + E009F80E230F387A004C3141 /* Products */ = { + isa = PBXGroup; + children = ( + E009F80D230F387A004C3141 /* EventSource.app */, + E009F821230F387B004C3141 /* EventSourceTests.xctest */, + 657A452723433EBA0017B99A /* EventSource.framework */, + ); + name = Products; + sourceTree = ""; + }; + E009F80F230F387A004C3141 /* EventSourceSample */ = { + isa = PBXGroup; + children = ( + E009F810230F387A004C3141 /* AppDelegate.swift */, + E009F85E230F3AA6004C3141 /* Main.storyboard */, + E009F85D230F3AA6004C3141 /* ViewController.swift */, + E009F819230F387B004C3141 /* LaunchScreen.storyboard */, + E009F81C230F387B004C3141 /* Info.plist */, + E009F817230F387B004C3141 /* Assets.xcassets */, + ); + path = EventSourceSample; + sourceTree = ""; + }; + E009F824230F387B004C3141 /* EventSourceTests */ = { + isa = PBXGroup; + children = ( + E0AC96D6231293C500DC89E4 /* Mock */, + E009F825230F387B004C3141 /* EventSourceTests.swift */, + E009F857230F3965004C3141 /* EventStreamParserTests.swift */, + E009F855230F3948004C3141 /* EventTests.swift */, + E009F827230F387C004C3141 /* Info.plist */, + ); + path = EventSourceTests; + sourceTree = ""; + }; + E009F83E230F38C0004C3141 /* EventSource */ = { + isa = PBXGroup; + children = ( + E009F83F230F38E9004C3141 /* Event.swift */, + E009F840230F38E9004C3141 /* EventSource.swift */, + E009F841230F38E9004C3141 /* EventStreamParser.swift */, + 657A452A23433EBA0017B99A /* Info.plist */, + ); + path = EventSource; + sourceTree = ""; + }; + E0AC96D6231293C500DC89E4 /* Mock */ = { + isa = PBXGroup; + children = ( + E0AC96D7231293D600DC89E4 /* URLSessionDataTaskMock.swift */, + ); + path = Mock; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 657A452223433EBA0017B99A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 657A452623433EBA0017B99A /* EventSource iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 657A452C23433EBA0017B99A /* Build configuration list for PBXNativeTarget "EventSource iOS" */; + buildPhases = ( + 657A452223433EBA0017B99A /* Headers */, + 657A452323433EBA0017B99A /* Sources */, + 657A452423433EBA0017B99A /* Frameworks */, + 657A452523433EBA0017B99A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "EventSource iOS"; + productName = EventSourceFramework; + productReference = 657A452723433EBA0017B99A /* EventSource.framework */; + productType = "com.apple.product-type.framework"; + }; + E009F80C230F387A004C3141 /* EventSource */ = { + isa = PBXNativeTarget; + buildConfigurationList = E009F835230F387C004C3141 /* Build configuration list for PBXNativeTarget "EventSource" */; + buildPhases = ( + E009F809230F387A004C3141 /* Sources */, + E009F80A230F387A004C3141 /* Frameworks */, + E009F80B230F387A004C3141 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = EventSource; + productName = EventSource; + productReference = E009F80D230F387A004C3141 /* EventSource.app */; + productType = "com.apple.product-type.application"; + }; + E009F820230F387B004C3141 /* EventSourceTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E009F838230F387C004C3141 /* Build configuration list for PBXNativeTarget "EventSourceTests" */; + buildPhases = ( + E009F81D230F387B004C3141 /* Sources */, + E009F81E230F387B004C3141 /* Frameworks */, + E009F81F230F387B004C3141 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E009F823230F387B004C3141 /* PBXTargetDependency */, + ); + name = EventSourceTests; + productName = EventSourceTests; + productReference = E009F821230F387B004C3141 /* EventSourceTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E009F805230F387A004C3141 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1030; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = Andres; + TargetAttributes = { + 657A452623433EBA0017B99A = { + CreatedOnToolsVersion = 11.0; + }; + E009F80C230F387A004C3141 = { + CreatedOnToolsVersion = 10.3; + }; + E009F820230F387B004C3141 = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = E009F808230F387A004C3141 /* Build configuration list for PBXProject "EventSource" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E009F804230F387A004C3141; + productRefGroup = E009F80E230F387A004C3141 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E009F80C230F387A004C3141 /* EventSource */, + E009F820230F387B004C3141 /* EventSourceTests */, + 657A452623433EBA0017B99A /* EventSource iOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 657A452523433EBA0017B99A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E009F80B230F387A004C3141 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E009F860230F3AA6004C3141 /* Main.storyboard in Resources */, + E009F81B230F387B004C3141 /* LaunchScreen.storyboard in Resources */, + E009F818230F387B004C3141 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E009F81F230F387B004C3141 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 657A452323433EBA0017B99A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 657A452F23433EDD0017B99A /* Event.swift in Sources */, + 657A453023433EDD0017B99A /* EventSource.swift in Sources */, + 657A453123433EDD0017B99A /* EventStreamParser.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E009F809230F387A004C3141 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E009F85F230F3AA6004C3141 /* ViewController.swift in Sources */, + E009F842230F38E9004C3141 /* Event.swift in Sources */, + E009F811230F387A004C3141 /* AppDelegate.swift in Sources */, + E009F844230F38E9004C3141 /* EventStreamParser.swift in Sources */, + E009F843230F38E9004C3141 /* EventSource.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E009F81D230F387B004C3141 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E009F858230F3965004C3141 /* EventStreamParserTests.swift in Sources */, + E0AC96D8231293D700DC89E4 /* URLSessionDataTaskMock.swift in Sources */, + E009F856230F3948004C3141 /* EventTests.swift in Sources */, + E0AC96DA2312A22200DC89E4 /* EventSource.swift in Sources */, + E009F826230F387B004C3141 /* EventSourceTests.swift in Sources */, + E0AC96DB2312A22200DC89E4 /* EventStreamParser.swift in Sources */, + E0AC96D92312A22200DC89E4 /* Event.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + E009F823230F387B004C3141 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E009F80C230F387A004C3141 /* EventSource */; + targetProxy = E009F822230F387B004C3141 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + E009F819230F387B004C3141 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + E009F81A230F387B004C3141 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 657A452D23433EBA0017B99A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = EventSource/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ar.com.andres.EventSource; + PRODUCT_NAME = EventSource; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 657A452E23433EBA0017B99A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = EventSource/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ar.com.andres.EventSource; + PRODUCT_NAME = EventSource; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + E009F833230F387C004C3141 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + E009F834230F387C004C3141 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + E009F836230F387C004C3141 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BB9PXVJ39L; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + INFOPLIST_FILE = EventSourceSample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ar.com.andres.EventSource; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E009F837230F387C004C3141 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BB9PXVJ39L; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/iOS", + ); + INFOPLIST_FILE = EventSourceSample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ar.com.andres.EventSource; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + E009F839230F387C004C3141 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BB9PXVJ39L; + INFOPLIST_FILE = EventSourceTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ar.com.andres.EventSourceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E009F83A230F387C004C3141 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = BB9PXVJ39L; + INFOPLIST_FILE = EventSourceTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = ar.com.andres.EventSourceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 657A452C23433EBA0017B99A /* Build configuration list for PBXNativeTarget "EventSource iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 657A452D23433EBA0017B99A /* Debug */, + 657A452E23433EBA0017B99A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E009F808230F387A004C3141 /* Build configuration list for PBXProject "EventSource" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E009F833230F387C004C3141 /* Debug */, + E009F834230F387C004C3141 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E009F835230F387C004C3141 /* Build configuration list for PBXNativeTarget "EventSource" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E009F836230F387C004C3141 /* Debug */, + E009F837230F387C004C3141 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E009F838230F387C004C3141 /* Build configuration list for PBXNativeTarget "EventSourceTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E009F839230F387C004C3141 /* Debug */, + E009F83A230F387C004C3141 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E009F805230F387A004C3141 /* Project object */; +} diff --git a/EventSource.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/EventSource.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..690f971 --- /dev/null +++ b/EventSource.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/EventSource.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/EventSource.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/EventSource.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/EventSource.xcodeproj/project.xcworkspace/xcuserdata/green.xcuserdatad/UserInterfaceState.xcuserstate b/EventSource.xcodeproj/project.xcworkspace/xcuserdata/green.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..66fc3bf Binary files /dev/null and b/EventSource.xcodeproj/project.xcworkspace/xcuserdata/green.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/EventSource.xcodeproj/xcshareddata/xcschemes/EventSource iOS.xcscheme b/EventSource.xcodeproj/xcshareddata/xcschemes/EventSource iOS.xcscheme new file mode 100644 index 0000000..b9fad2c --- /dev/null +++ b/EventSource.xcodeproj/xcshareddata/xcschemes/EventSource iOS.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EventSource.xcodeproj/xcshareddata/xcschemes/EventSource.xcscheme b/EventSource.xcodeproj/xcshareddata/xcschemes/EventSource.xcscheme new file mode 100644 index 0000000..6858f79 --- /dev/null +++ b/EventSource.xcodeproj/xcshareddata/xcschemes/EventSource.xcscheme @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EventSource.xcodeproj/xcuserdata/green.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/EventSource.xcodeproj/xcuserdata/green.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..46f6db7 --- /dev/null +++ b/EventSource.xcodeproj/xcuserdata/green.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EventSourceHttpBody.podspec b/EventSourceHttpBody.podspec new file mode 100644 index 0000000..7d29267 --- /dev/null +++ b/EventSourceHttpBody.podspec @@ -0,0 +1,17 @@ +Pod::Spec.new do |s| + s.name = 'EventSourceHttpBody' + s.version = '0.1.0' + s.summary = 'EventSource' + + s.homepage = "https://github.com/exyte/EventSourceHttpBody" + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'Exyte' => 'info@exyte.com' } + s.source = { :git => 'https://github.com/exyte/EventSourceHttpBody', :tag => s.version.to_s } + s.social_media_url = 'http://exyte.com' + + s.ios.deployment_target = '12.0' + + s.source_files = 'EventSourceHttpBody/*.swift' + s.swift_version = "5.2" + +end diff --git a/EventSourceHttpBody/.DS_Store b/EventSourceHttpBody/.DS_Store new file mode 100644 index 0000000..d220799 Binary files /dev/null and b/EventSourceHttpBody/.DS_Store differ diff --git a/EventSourceHttpBody/Event.swift b/EventSourceHttpBody/Event.swift new file mode 100644 index 0000000..1427cbb --- /dev/null +++ b/EventSourceHttpBody/Event.swift @@ -0,0 +1,103 @@ +// +// Event.swift +// EventSource +// +// Created by Andres on 01/06/2019. +// Copyright © 2019 inaka. All rights reserved. +// + +import Foundation + +enum Event { + case event(id: String?, event: String?, data: String?, time: String?) + + init?(eventString: String?, newLineCharacters: [String]) { + guard let eventString = eventString else { return nil } + + if eventString.hasPrefix(":") { + return nil + } + + self = Event.parseEvent(eventString, newLineCharacters: newLineCharacters) + } + + var id: String? { + guard case let .event(eventId, _, _, _) = self else { return nil } + return eventId + } + + var event: String? { + guard case let .event(_, eventName, _, _) = self else { return nil } + return eventName + } + + var data: String? { + guard case let .event(_, _, eventData, _) = self else { return nil } + return eventData + } + + var retryTime: Int? { + guard case let .event(_, _, _, aTime) = self, let time = aTime else { return nil } + return Int(time.trimmingCharacters(in: CharacterSet.whitespaces)) + } + + var onlyRetryEvent: Bool? { + guard case let .event(id, name, data, time) = self else { return nil } + let otherThanTime = id ?? name ?? data + + if otherThanTime == nil && time != nil { + return true + } + + return false + + } +} + +private extension Event { + + static func parseEvent(_ eventString: String, newLineCharacters: [String]) -> Event { + var event: [String: String?] = [:] + + for line in eventString.components(separatedBy: CharacterSet.newlines) as [String] { + let (akey, value) = Event.parseLine(line, newLineCharacters: newLineCharacters) + guard let key = akey else { continue } + + if let value = value, let previousValue = event[key] ?? nil { + event[key] = "\(previousValue)\n\(value)" + } else if let value = value { + event[key] = value + } else { + event[key] = nil + } + } + + // the only possible field names for events are: id, event and data. Everything else is ignored. + return .event( + id: event["id"] ?? nil, + event: event["event"] ?? nil, + data: event["data"] ?? nil, + time: event["retry"] ?? nil + ) + } + + static func parseLine(_ line: String, newLineCharacters: [String]) -> (key: String?, value: String?) { + var key: NSString?, value: NSString? + let scanner = Scanner(string: line) + scanner.scanUpTo(":", into: &key) + scanner.scanString(":", into: nil) + + for newline in newLineCharacters { + if scanner.scanUpTo(newline, into: &value) { + break + } + } + + // for id and data if they come empty they should return an empty string value. + if key != "event" && value == nil { + value = "" + } + + return (key as String?, value as String?) + } +} diff --git a/EventSourceHttpBody/EventSource.swift b/EventSourceHttpBody/EventSource.swift new file mode 100644 index 0000000..cb80ee0 --- /dev/null +++ b/EventSourceHttpBody/EventSource.swift @@ -0,0 +1,262 @@ +// +// EventSource.swift +// EventSource +// +// Created by Andres on 2/13/15. +// Copyright (c) 2015 Inaka. All rights reserved. +// + +import Foundation + +public enum EventSourceState { + case connecting + case open + case closed +} + +public protocol EventSourceProtocol { + var headers: [String: String] { get } + + /// RetryTime: This can be changed remotly if the server sends an event `retry:` + var retryTime: Int { get } + + /// URL where EventSource will listen for events. + var url: URL { get } + + /// The last event id received from server. This id is neccesary to keep track of the last event-id received to avoid + /// receiving duplicate events after a reconnection. + var lastEventId: String? { get } + + /// Current state of EventSource + var readyState: EventSourceState { get } + + /// Method used to connect to server. It can receive an optional lastEventId indicating the Last-Event-ID + /// + /// - Parameter lastEventId: optional value that is going to be added on the request header to server. + func connect(lastEventId: String?) + + /// Method used to disconnect from server. + func disconnect() + + /// Returns the list of event names that we are currently listening for. + /// + /// - Returns: List of event names. + func events() -> [String] + + /// Callback called when EventSource has successfully connected to the server. + /// + /// - Parameter onOpenCallback: callback + func onOpen(_ onOpenCallback: @escaping (() -> Void)) + + /// Callback called once EventSource has disconnected from server. This can happen for multiple reasons. + /// The server could have requested the disconnection or maybe a network layer error, wrong URL or any other + /// error. The callback receives as parameters the status code of the disconnection, if we should reconnect or not + /// following event source rules and finally the network layer error if any. All this information is more than + /// enought for you to take a decition if you should reconnect or not. + /// - Parameter onOpenCallback: callback + func onComplete(_ onComplete: @escaping ((Int?, Bool?, NSError?) -> Void)) + + /// This callback is called everytime an event with name "message" or no name is received. + func onMessage(_ onMessageCallback: @escaping ((_ id: String?, _ event: String?, _ data: String?) -> Void)) + + /// Add an event handler for an specific event name. + /// + /// - Parameters: + /// - event: name of the event to receive + /// - handler: this handler will be called everytime an event is received with this event-name + func addEventListener(_ event: String, + handler: @escaping ((_ id: String?, _ event: String?, _ data: String?) -> Void)) + + /// Remove an event handler for the event-name + /// + /// - Parameter event: name of the listener to be remove from event source. + func removeEventListener(_ event: String) +} + +open class EventSource: NSObject, EventSourceProtocol, URLSessionDataDelegate { + static let DefaultRetryTime = 3000 + + public let urlRequest: URLRequest + public var url: URL { urlRequest.url! } + private(set) public var lastEventId: String? + private(set) public var retryTime = EventSource.DefaultRetryTime + private(set) public var headers: [String: String] + private(set) public var readyState: EventSourceState + + private var onOpenCallback: (() -> Void)? + private var onComplete: ((Int?, Bool?, NSError?) -> Void)? + private var onMessageCallback: ((_ id: String?, _ event: String?, _ data: String?) -> Void)? + private var eventListeners: [String: (_ id: String?, _ event: String?, _ data: String?) -> Void] = [:] + + private var eventStreamParser: EventStreamParser? + private var operationQueue: OperationQueue + private var mainQueue = DispatchQueue.main + private var urlSession: URLSession? + + public init( + urlRequest: URLRequest + ) { + self.urlRequest = urlRequest + self.headers = urlRequest.allHTTPHeaderFields ?? [:] + + readyState = EventSourceState.closed + operationQueue = OperationQueue() + operationQueue.maxConcurrentOperationCount = 1 + + super.init() + } + + public func connect(lastEventId: String? = nil) { + eventStreamParser = EventStreamParser() + readyState = .connecting + + let configuration = sessionConfiguration(lastEventId: lastEventId) + urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: operationQueue) + urlSession?.dataTask(with: urlRequest).resume() + } + + public func disconnect() { + readyState = .closed + urlSession?.invalidateAndCancel() + } + + public func onOpen(_ onOpenCallback: @escaping (() -> Void)) { + self.onOpenCallback = onOpenCallback + } + + public func onComplete(_ onComplete: @escaping ((Int?, Bool?, NSError?) -> Void)) { + self.onComplete = onComplete + } + + public func onMessage(_ onMessageCallback: @escaping ((_ id: String?, _ event: String?, _ data: String?) -> Void)) { + self.onMessageCallback = onMessageCallback + } + + public func addEventListener(_ event: String, + handler: @escaping ((_ id: String?, _ event: String?, _ data: String?) -> Void)) { + eventListeners[event] = handler + } + + public func removeEventListener(_ event: String) { + eventListeners.removeValue(forKey: event) + } + + public func events() -> [String] { + return Array(eventListeners.keys) + } + + open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + + if readyState != .open { + return + } + + if let events = eventStreamParser?.append(data: data) { + notifyReceivedEvents(events) + } + } + + open func urlSession(_ session: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { + + completionHandler(URLSession.ResponseDisposition.allow) + + readyState = .open + mainQueue.async { [weak self] in self?.onOpenCallback?() } + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + didCompleteWithError error: Error?) { + + guard let responseStatusCode = (task.response as? HTTPURLResponse)?.statusCode else { + mainQueue.async { [weak self] in self?.onComplete?(nil, nil, error as NSError?) } + return + } + + let reconnect = shouldReconnect(statusCode: responseStatusCode) + mainQueue.async { [weak self] in self?.onComplete?(responseStatusCode, reconnect, nil) } + } + + open func urlSession(_ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) { + + var newRequest = request + self.headers.forEach { newRequest.setValue($1, forHTTPHeaderField: $0) } + completionHandler(newRequest) + } +} + +internal extension EventSource { + + func sessionConfiguration(lastEventId: String?) -> URLSessionConfiguration { + + var additionalHeaders = headers + if let eventID = lastEventId { + additionalHeaders["Last-Event-Id"] = eventID + } + + additionalHeaders["Accept"] = "text/event-stream" + additionalHeaders["Cache-Control"] = "no-cache" + + let sessionConfiguration = URLSessionConfiguration.default + sessionConfiguration.timeoutIntervalForRequest = TimeInterval(INT_MAX) + sessionConfiguration.timeoutIntervalForResource = TimeInterval(INT_MAX) + sessionConfiguration.httpAdditionalHeaders = additionalHeaders + + return sessionConfiguration + } + + func readyStateOpen() { + readyState = .open + } +} + +private extension EventSource { + + func notifyReceivedEvents(_ events: [Event]) { + + for event in events { + lastEventId = event.id + retryTime = event.retryTime ?? EventSource.DefaultRetryTime + + if event.onlyRetryEvent == true { + continue + } + + if event.event == nil || event.event == "thread.message.delta" { + mainQueue.async { [weak self] in self?.onMessageCallback?(event.id, "thread.message.delta", event.data) } + } + + if let eventName = event.event, let eventHandler = eventListeners[eventName] { + mainQueue.async { eventHandler(event.id, event.event, event.data) } + } + } + } + + // Following "5 Processing model" from: + // https://www.w3.org/TR/2009/WD-eventsource-20090421/#handler-eventsource-onerror + func shouldReconnect(statusCode: Int) -> Bool { + switch statusCode { + case 200: + return false + case _ where statusCode > 200 && statusCode < 300: + return true + default: + return false + } + } +} + +public extension EventSourceProtocol { + var retryTime: Int { + return 5 + } +} + + diff --git a/EventSourceHttpBody/EventStreamParser.swift b/EventSourceHttpBody/EventStreamParser.swift new file mode 100644 index 0000000..515c22b --- /dev/null +++ b/EventSourceHttpBody/EventStreamParser.swift @@ -0,0 +1,87 @@ +// +// EventStreamParser.swift +// EventSource +// +// Created by Andres on 30/05/2019. +// Copyright © 2019 inaka. All rights reserved. +// + +import Foundation + +final class EventStreamParser { + + // Events are separated by end of line. End of line can be: + // \r = CR (Carriage Return) → Used as a new line character in Mac OS before X + // \n = LF (Line Feed) → Used as a new line character in Unix/Mac OS X + // \r\n = CR + LF → Used as a new line character in Windows + private let validNewlineCharacters = ["\r\n", "\n", "\r"] + private let dataBuffer: NSMutableData + + init() { + dataBuffer = NSMutableData() + } + + var currentBuffer: String? { + return NSString(data: dataBuffer as Data, encoding: String.Encoding.utf8.rawValue) as String? + } + + func append(data: Data?) -> [Event] { + guard let data = data else { return [] } + dataBuffer.append(data) + + let events = extractEventsFromBuffer().compactMap { [weak self] eventString -> Event? in + guard let self = self else { return nil } + return Event(eventString: eventString, newLineCharacters: self.validNewlineCharacters) + } + + return events + } + + private func extractEventsFromBuffer() -> [String] { + var events = [String]() + + var searchRange = NSRange(location: 0, length: dataBuffer.length) + while let foundRange = searchFirstEventDelimiter(in: searchRange) { + // if we found a delimiter range that means that from the beggining of the buffer + // until the beggining of the range where the delimiter was found we have an event. + // The beggining of the event is: searchRange.location + // The lenght of the event is the position where the foundRange was found. + + let dataChunk = dataBuffer.subdata( + with: NSRange(location: searchRange.location, length: foundRange.location - searchRange.location) + ) + + if let text = String(bytes: dataChunk, encoding: .utf8) { + events.append(text) + } + + // We move the searchRange start position (location) after the fundRange we just found and + searchRange.location = foundRange.location + foundRange.length + searchRange.length = dataBuffer.length - searchRange.location + } + + // We empty the piece of the buffer we just search in. + dataBuffer.replaceBytes(in: NSRange(location: 0, length: searchRange.location), withBytes: nil, length: 0) + + return events + } + + // This methods returns the range of the first delimiter found in the buffer. For example: + // If in the buffer we have: `id: event-id-1\ndata:event-data-first\n\n` + // This method will return the range for the `\n\n`. + private func searchFirstEventDelimiter(in range: NSRange) -> NSRange? { + let delimiters = validNewlineCharacters.map { "\($0)\($0)".data(using: String.Encoding.utf8)! } + + for delimiter in delimiters { + let foundRange = dataBuffer.range( + of: delimiter, options: NSData.SearchOptions(), in: range + ) + + if foundRange.location != NSNotFound { + return foundRange + } + } + + return nil + } +} diff --git a/EventSourceHttpBody/Info.plist b/EventSourceHttpBody/Info.plist new file mode 100644 index 0000000..9bcb244 --- /dev/null +++ b/EventSourceHttpBody/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/EventSourceSample/AppDelegate.swift b/EventSourceSample/AppDelegate.swift new file mode 100644 index 0000000..bf65ced --- /dev/null +++ b/EventSourceSample/AppDelegate.swift @@ -0,0 +1,16 @@ +// +// AppDelegate.swift +// EventSource +// +// Created by Andres on 22/08/2019. +// Copyright © 2019 Andres. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + +} diff --git a/EventSourceSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/EventSourceSample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d8db8d6 --- /dev/null +++ b/EventSourceSample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/EventSourceSample/Assets.xcassets/Contents.json b/EventSourceSample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/EventSourceSample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/EventSourceSample/Base.lproj/LaunchScreen.storyboard b/EventSourceSample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..bfa3612 --- /dev/null +++ b/EventSourceSample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EventSourceSample/Info.plist b/EventSourceSample/Info.plist new file mode 100644 index 0000000..16be3b6 --- /dev/null +++ b/EventSourceSample/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/EventSourceSample/Main.storyboard b/EventSourceSample/Main.storyboard new file mode 100644 index 0000000..e1e9e0b --- /dev/null +++ b/EventSourceSample/Main.storyboard @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EventSourceSample/ViewController.swift b/EventSourceSample/ViewController.swift new file mode 100644 index 0000000..da43755 --- /dev/null +++ b/EventSourceSample/ViewController.swift @@ -0,0 +1,90 @@ +// +// ViewController.swift +// EventSource +// +// Created by Andres on 2/13/15. +// Copyright (c) 2015 Inaka. All rights reserved. +// + +import UIKit +class ViewController: UIViewController { + + @IBOutlet fileprivate weak var status: UILabel! + @IBOutlet fileprivate weak var dataLabel: UILabel! + @IBOutlet fileprivate weak var nameLabel: UILabel! + @IBOutlet fileprivate weak var idLabel: UILabel! + @IBOutlet fileprivate weak var squareConstraint: NSLayoutConstraint! + var eventSource: EventSource? + + override func viewDidLoad() { + super.viewDidLoad() + + var urlRequest = URLRequest(url: URL(string: "http://127.0.0.1:8080/sse")!) + urlRequest.httpMethod = "POST" + urlRequest.addValue("Bearer basic-auth-token", forHTTPHeaderField: "Authorization") + + eventSource = EventSource(urlRequest: urlRequest) + + eventSource?.connect() + + eventSource?.onOpen { [weak self] in + self?.status.backgroundColor = UIColor(red: 166/255, green: 226/255, blue: 46/255, alpha: 1) + self?.status.text = "CONNECTED" + } + + eventSource?.onComplete { [weak self] statusCode, reconnect, error in + self?.status.backgroundColor = UIColor(red: 249/255, green: 38/255, blue: 114/255, alpha: 1) + self?.status.text = "DISCONNECTED" + + guard reconnect ?? false else { return } + + let retryTime = self?.eventSource?.retryTime ?? 3000 + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(retryTime)) { [weak self] in + self?.eventSource?.connect() + } + } + + eventSource?.onMessage { [weak self] id, event, data in + self?.updateLabels(id, event: event, data: data) + } + + eventSource?.addEventListener("user-connected") { [weak self] id, event, data in + self?.updateLabels(id, event: event, data: data) + } + } + + func updateLabels(_ id: String?, event: String?, data: String?) { + idLabel.text = id + nameLabel.text = event + dataLabel.text = data + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + let finalPosition = view.frame.size.width - 50 + + squareConstraint.constant = 0 + view.layoutIfNeeded() + + let animationOptions: UIView.KeyframeAnimationOptions = [ + UIView.KeyframeAnimationOptions.repeat, UIView.KeyframeAnimationOptions.autoreverse + ] + + UIView.animateKeyframes(withDuration: 2, + delay: 0, + options: animationOptions, + animations: { () in + self.squareConstraint.constant = finalPosition + self.view.layoutIfNeeded() + }, completion: nil) + } + + @IBAction func disconnect(_ sender: Any) { + eventSource?.disconnect() + } + + @IBAction func connect(_ sender: Any) { + eventSource?.connect() + } +} diff --git a/EventSourceTests/EventSourceTests.swift b/EventSourceTests/EventSourceTests.swift new file mode 100644 index 0000000..ad04bee --- /dev/null +++ b/EventSourceTests/EventSourceTests.swift @@ -0,0 +1,184 @@ +// +// EventSourceTests.swift +// EventSourceTests +// +// Created by Andres on 22/08/2019. +// Copyright © 2019 Andres. All rights reserved. +// + +import XCTest +@testable import EventSource + +class EventSourceTests: XCTestCase { + + var eventSource: EventSource! + let url = URL(string: "https://localhost")! + + override func setUp() { + eventSource = EventSource(url: url, headers: ["header": "value"]) + } + + func testCreation() { + XCTAssertEqual(url, eventSource.url) + XCTAssertEqual(eventSource.headers, ["header": "value"]) + XCTAssertEqual(eventSource.readyState, EventSourceState.closed) + } + + func testDisconnect() { + XCTAssertEqual(eventSource.readyState, EventSourceState.closed) + } + + func testSessionConfiguration() { + let configuration = eventSource.sessionConfiguration(lastEventId: "event-id") + + XCTAssertEqual(configuration.timeoutIntervalForRequest, TimeInterval(INT_MAX)) + XCTAssertEqual(configuration.timeoutIntervalForResource, TimeInterval(INT_MAX)) + XCTAssertEqual(configuration.httpAdditionalHeaders as? [String: String], [ + "Last-Event-Id": "event-id", "Accept": "text/event-stream", "Cache-Control": "no-cache", "header": "value"] + ) + } + + func testAddEventListener() { + eventSource.addEventListener("event-name") { _, _, _ in } + XCTAssertEqual(eventSource.events(), ["event-name"]) + } + + func testRemoveEventListener() { + eventSource.addEventListener("event-name") { _, _, _ in } + eventSource.removeEventListener("event-name") + + XCTAssertEqual(eventSource.events(), []) + } + + func testRetryTime() { + eventSource.connect() + eventSource.readyStateOpen() + + let aSession = URLSession(configuration: URLSessionConfiguration.default) + let aSessionDataTask = URLSessionDataTask() + + let event = """ + id: event-id-1 + data: event-data-first + retry: 1000 + + id: event + """ + + let data = event.data(using: .utf8)! + eventSource.urlSession(aSession, dataTask: aSessionDataTask, didReceive: data) + XCTAssertEqual(eventSource.retryTime, 1000) + } + + func testOnOpen() { + let expectation = XCTestExpectation(description: "onOpen gets called") + + eventSource.onOpen { + XCTAssertEqual(self.eventSource.readyState, EventSourceState.open) + expectation.fulfill() + } + + let aSession = URLSession(configuration: URLSessionConfiguration.default) + let aSessionDataTask = URLSessionDataTask() + let urlResponse = URLResponse(url: url, mimeType: nil, expectedContentLength: 0, textEncodingName: nil) + eventSource.urlSession(aSession, dataTask: aSessionDataTask, didReceive: urlResponse) { _ in } + wait(for: [expectation], timeout: 2.0) + } + + func testOnCompleteRetryTrue() { + let expectation = XCTestExpectation(description: "onComplete gets called") + eventSource.onComplete { statusCode, retry, _ in + XCTAssertEqual(statusCode, 200) + XCTAssertEqual(retry, false) + expectation.fulfill() + } + + let aSession = URLSession(configuration: URLSessionConfiguration.default) + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: [:]) + let dataTask = URLSessionDataTaskMock(response: response) + eventSource.urlSession(aSession, task: dataTask, didCompleteWithError: nil) + wait(for: [expectation], timeout: 2.0) + } + + func testOnCompleteRetryFalse() { + let expectation = XCTestExpectation(description: "onComplete gets called") + eventSource.onComplete { statusCode, retry, _ in + XCTAssertEqual(statusCode, 250) + XCTAssertEqual(retry, true) + expectation.fulfill() + } + + let aSession = URLSession(configuration: URLSessionConfiguration.default) + let response = HTTPURLResponse(url: url, statusCode: 250, httpVersion: nil, headerFields: [:]) + let dataTask = URLSessionDataTaskMock(response: response) + eventSource.urlSession(aSession, task: dataTask, didCompleteWithError: nil) + wait(for: [expectation], timeout: 2.0) + } + + func testOnCompleteError() { + let expectation = XCTestExpectation(description: "onComplete gets called") + eventSource.onComplete { statusCode, retry, error in + XCTAssertNotNil(error) + XCTAssertNil(retry) + XCTAssertNil(statusCode) + expectation.fulfill() + } + + let aSession = URLSession(configuration: URLSessionConfiguration.default) + let dataTask = URLSessionDataTaskMock(response: nil) + let error = NSError(domain: "", code: -1, userInfo: [:]) + eventSource.urlSession(aSession, task: dataTask, didCompleteWithError: error) + wait(for: [expectation], timeout: 2.0) + } + + func testSmallEventStream() { + eventSource.connect() + eventSource.readyStateOpen() + + var eventsIds: [String] = [] + var eventNames: [String] = [] + var eventDatas: [String] = [] + + let exp = self.expectation(description: "onMessage gets called") + eventSource.onMessage { eventId, eventName, eventData in + eventsIds.append(eventId ?? "") + eventNames.append(eventName ?? "") + eventDatas.append(eventData ?? "") + + if(eventsIds.count == 3) { + exp.fulfill() + } + } + + let eventsString = """ + id: event-id-1 + data: event-data-first + + id: event-id-2 + data: event-data-second + + id: event-id-3 + data: event-data-third + + + """ + + let aSession = URLSession(configuration: URLSessionConfiguration.default) + let aSessionDataTask = URLSessionDataTask() + let data = eventsString.data(using: .utf8)! + eventSource.urlSession(aSession, dataTask: aSessionDataTask, didReceive: data) + + waitForExpectations(timeout: 2) { _ in + XCTAssertEqual(eventsIds, ["event-id-1", "event-id-2", "event-id-3"]) + XCTAssertEqual(eventNames, ["message", "message", "message"]) + XCTAssertEqual(eventsIds, ["event-id-1", "event-id-2", "event-id-3"]) + } + } + + func testDisconnet() { + eventSource.readyStateOpen() + eventSource.disconnect() + + XCTAssertEqual(eventSource.readyState, .closed) + } +} diff --git a/EventSourceTests/EventStreamParserTests.swift b/EventSourceTests/EventStreamParserTests.swift new file mode 100644 index 0000000..7adc42e --- /dev/null +++ b/EventSourceTests/EventStreamParserTests.swift @@ -0,0 +1,209 @@ +// +// EventParserTests.swift +// EventSourceTests +// +// Created by Andres on 30/05/2019. +// Copyright © 2019 inaka. All rights reserved. +// + +import UIKit +import XCTest +@testable import EventSource + +class EventStreamParserTests: XCTestCase { + + func testExtractingEvents() { + let eventParser = EventStreamParser() + let eventsString = """ + id: event-id-1 + data: event-data-first + + id: event-id-2 + data: event-data-second + + id: event-id-3 + data: event-data-third + """ + + let events = eventParser.append(data: eventsString.data(using: .utf8)) + XCTAssertEqual(events.count, 2) + } + + func testExtractingSplittedEvents() { + let eventParser = EventStreamParser() + let firstEventsStringPiece = """ + id: event-id-1 + data: event-data-first + + id: event + """ + + var events = eventParser.append(data: firstEventsStringPiece.data(using: .utf8)) + XCTAssertEqual(events.count, 1) + + let secondEventsStringPiece = """ + -id-2 + data: event-data-second + + id: event-id-3 + data: event-data-third + + + """ + + events = eventParser.append(data: secondEventsStringPiece.data(using: .utf8)) + XCTAssertEqual(events.count, 2) + } + + // The following test streams are the samples introduced in the https://www.w3.org/TR/eventsource/ from section 7 + func testFirstW3StreamSample() { + let eventParser = EventStreamParser() + let eventString = """ + data: YHOO + data: +2 + data: 10 + + + """ + + let events = eventParser.append(data: eventString.data(using: .utf8)) + XCTAssertEqual(events.count, 1) + + let event = events.first! + XCTAssertEqual(event.id, nil) + XCTAssertEqual(event.event, nil) + XCTAssertEqual(event.retryTime, nil) + XCTAssertEqual(event.data, "YHOO\n+2\n10") + } + + // The first block has just a comment, and will fire nothing. + // The second block has two fields with names "data" and "id" respectively; an event will be fired for this block. + // The third block fires an event. + // The last block is not fired as a new breakline is still missing to be parsed. + func testSecondW3StreamSample() { + let eventParser = EventStreamParser() + let eventString = """ + : test stream + + data: first event + id: 1 + + data:second event + id + + data: third event + + """ + + let events = eventParser.append(data: eventString.data(using: .utf8)) + XCTAssertEqual(events.count, 2) + + let firstEvent = events[0] + XCTAssertEqual(firstEvent.id, "1") + XCTAssertEqual(firstEvent.event, nil) + XCTAssertEqual(firstEvent.data, "first event") + XCTAssertEqual(firstEvent.retryTime, nil) + + let secondEvent = events[1] + XCTAssertEqual(secondEvent.id, "") + XCTAssertEqual(secondEvent.event, nil) + XCTAssertEqual(secondEvent.data, "second event") + XCTAssertEqual(secondEvent.retryTime, nil) + } + + // The first block fires an event with the data set to the empty string. + // The middle block fires an event with the data set to a single newline character. + // The last block fires an event with the data set to empty string. + func testThirdW3StreamSample() { + let eventParser = EventStreamParser() + let eventString = """ + data + + data + data + + data: + + + """ + + let events = eventParser.append(data: eventString.data(using: .utf8)) + XCTAssertEqual(events.count, 3) + + let firstEvent = events[0] + XCTAssertEqual(firstEvent.id, nil) + XCTAssertEqual(firstEvent.event, nil) + XCTAssertEqual(firstEvent.data, "") + XCTAssertEqual(firstEvent.retryTime, nil) + + let secondEvent = events[1] + XCTAssertEqual(secondEvent.id, nil) + XCTAssertEqual(secondEvent.event, nil) + XCTAssertEqual(secondEvent.data, "\n") + XCTAssertEqual(secondEvent.retryTime, nil) + + let thirdEvent = events[2] + XCTAssertEqual(thirdEvent.id, nil) + XCTAssertEqual(thirdEvent.event, nil) + XCTAssertEqual(thirdEvent.data, "") + XCTAssertEqual(thirdEvent.retryTime, nil) + } + + // The following stream fires two identical events: + func testForthW3StreamSample() { + let eventParser = EventStreamParser() + let eventString = """ + data:test + + data: test + + + """ + + let events = eventParser.append(data: eventString.data(using: .utf8)) + XCTAssertEqual(events.count, 2) + + let firstEvent = events[0] + XCTAssertEqual(firstEvent.id, nil) + XCTAssertEqual(firstEvent.event, nil) + XCTAssertEqual(firstEvent.data, "test") + XCTAssertEqual(firstEvent.retryTime, nil) + + let secondEvent = events[1] + XCTAssertEqual(secondEvent.id, nil) + XCTAssertEqual(secondEvent.event, nil) + XCTAssertEqual(secondEvent.data, "test") + XCTAssertEqual(secondEvent.retryTime, nil) + } + + func testEventAfterCommentedLine() { + let eventParser = EventStreamParser() + let eventString = """ + :thump + event: update + data: data-from-the-event + + + """ + + let events = eventParser.append(data: eventString.data(using: .utf8)) + XCTAssertEqual(events.count, 1) + + let firstEvent = events[0] + XCTAssertEqual(firstEvent.id, nil) + XCTAssertEqual(firstEvent.event, "update") + XCTAssertEqual(firstEvent.data, "data-from-the-event") + XCTAssertEqual(firstEvent.retryTime, nil) + } + + func testCurrentBuffer() { + let eventParser = EventStreamParser() + let eventString = """ + -id-2 + data: event-data-second + + """ + _ = eventParser.append(data: eventString.data(using: .utf8)) + XCTAssertEqual(eventParser.currentBuffer, eventString) + } +} diff --git a/EventSourceTests/EventTests.swift b/EventSourceTests/EventTests.swift new file mode 100644 index 0000000..b77f79c --- /dev/null +++ b/EventSourceTests/EventTests.swift @@ -0,0 +1,154 @@ +// +// EventTests.swift +// EventSourceTests +// +// Created by Andres on 01/06/2019. +// Copyright © 2019 inaka. All rights reserved. +// + +import UIKit +import XCTest +@testable import EventSource + +class EventTests: XCTestCase { + let newLineCharacters = ["\r\n", "\n", "\r"] + + func testIgnoreComment() { + var event = Event(eventString: ":retry", newLineCharacters: newLineCharacters) + XCTAssertNil(event) + + event = Event(eventString: ": retry", newLineCharacters: newLineCharacters) + XCTAssertNil(event) + } + + func testRetryEvent() { + let eventsString = """ + retry: 5000 + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.retryTime, 5000) + } + + func testRetryEventWrongTime() { + let eventsString = """ + retry: this is a wrong value + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.retryTime, nil) + } + + func testFullBasicEvent() { + let eventsString = """ + id: event-id-1 + data: event-data-first + event: testing event name + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.id, "event-id-1") + XCTAssertEqual(event?.data, "event-data-first") + XCTAssertEqual(event?.event, "testing event name") + } + + func testFullBasicEventWithSpaces() { + let eventsString = """ + id: event-id-1 + data: event-data-first + event: testing event name + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.id, "event-id-1") + XCTAssertEqual(event?.data, "event-data-first") + XCTAssertEqual(event?.event, "testing event name") + } + + func testEventWithMultipleLinesOfData() { + let eventsString = """ + id: event-id-1 + data: first line + data: second line + data: third line + event: testing event name + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.id, "event-id-1") + XCTAssertEqual(event?.data, "first line\nsecond line\nthird line") + XCTAssertEqual(event?.event, "testing event name") + } + + func testEventWithNoId() { + let eventsString = """ + event: test-event + data: first line + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.id, nil) + XCTAssertEqual(event?.data, "first line") + XCTAssertEqual(event?.event, "test-event") + } + + func testEventWithEmptyId() { + let eventsString = """ + id + data: first line + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.id, "") + XCTAssertEqual(event?.data, "first line") + } + + func testEventWithEmptyData() { + let eventsString = "data" + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.data, "") + } + + func testEventDobleEmptyData() { + let eventsString = """ + data + data + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.data, "\n") + } + + func testEventWithRetryTime() { + let eventsString = """ + id + data + data + retry: 1000 + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.id, "") + XCTAssertEqual(event?.data, "\n") + XCTAssertEqual(event?.retryTime, 1000) + } + + func testOnlyRetryTime() { + let eventsString = """ + retry: 1000 + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.onlyRetryEvent, true) + } + + func testOnlyRetryTimeWithEventInfo() { + let eventsString = """ + retry: 1000 + id + """ + + let event = Event(eventString: eventsString, newLineCharacters: newLineCharacters) + XCTAssertEqual(event?.onlyRetryEvent, false) + } +} diff --git a/EventSourceTests/Info.plist b/EventSourceTests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/EventSourceTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/EventSourceTests/Mock/URLSessionDataTaskMock.swift b/EventSourceTests/Mock/URLSessionDataTaskMock.swift new file mode 100644 index 0000000..6effa5c --- /dev/null +++ b/EventSourceTests/Mock/URLSessionDataTaskMock.swift @@ -0,0 +1,24 @@ +// +// URLSessionDataTaskMock.swift +// EventSourceTests +// +// Created by Andres on 25/08/2019. +// Copyright © 2019 Andres. All rights reserved. +// + +import Foundation + +class URLSessionDataTaskMock: URLSessionDataTask { + + let mockResponse: URLResponse? + + init(response: URLResponse?) { + mockResponse = response + super.init() + } + + override var response: URLResponse? { + return mockResponse + } + +} diff --git a/LICENSE b/LICENSE index 6faaaff..74882e3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2024 Exyte - -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. +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2015 Erlang Solutions Ltd. + + 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. diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..2468390 --- /dev/null +++ b/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.1 +// The swift-tools-version declares the minimum version of Swift required to build this package. +import PackageDescription + +let package = Package( + name: "EventSource", + platforms: [ + .iOS("8.0"), + .macOS("10.10"), + ], + products: [ + // Products define the executables and libraries produced by a package, and make them visible to other packages. + .library( + name: "EventSource", + targets: ["EventSource"]), + ], + dependencies: [], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages which this package depends on. + .target( + name: "EventSource", + dependencies: [], + path: "EventSource"), + .testTarget( + name: "EventSourceTests", + dependencies: ["EventSource"], + path: "EventSourceTests"), + ] +) diff --git a/sse-server/node_modules/.bin/uuid b/sse-server/node_modules/.bin/uuid new file mode 120000 index 0000000..80eb14a --- /dev/null +++ b/sse-server/node_modules/.bin/uuid @@ -0,0 +1 @@ +../node-uuid/bin/uuid \ No newline at end of file diff --git a/sse-server/node_modules/.package-lock.json b/sse-server/node_modules/.package-lock.json new file mode 100644 index 0000000..d6887c9 --- /dev/null +++ b/sse-server/node_modules/.package-lock.json @@ -0,0 +1,17 @@ +{ + "name": "SSE-Example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/node-uuid": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.2.tgz", + "integrity": "sha1-kH2z0Rt7aiz0+QX7cZnxSuc3m6A=", + "deprecated": "Use uuid module instead", + "bin": { + "uuid": "bin/uuid" + } + } + } +} diff --git a/sse-server/node_modules/node-uuid/.npmignore b/sse-server/node_modules/node-uuid/.npmignore new file mode 100644 index 0000000..fd4f2b0 --- /dev/null +++ b/sse-server/node_modules/node-uuid/.npmignore @@ -0,0 +1,2 @@ +node_modules +.DS_Store diff --git a/sse-server/node_modules/node-uuid/LICENSE.md b/sse-server/node_modules/node-uuid/LICENSE.md new file mode 100644 index 0000000..652609b --- /dev/null +++ b/sse-server/node_modules/node-uuid/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2010-2012 Robert Kieffer + +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. diff --git a/sse-server/node_modules/node-uuid/README.md b/sse-server/node_modules/node-uuid/README.md new file mode 100644 index 0000000..b7d04c9 --- /dev/null +++ b/sse-server/node_modules/node-uuid/README.md @@ -0,0 +1,243 @@ +# node-uuid + +Simple, fast generation of [RFC4122](http://www.ietf.org/rfc/rfc4122.txt) UUIDS. + +Features: + +* Generate RFC4122 version 1 or version 4 UUIDs +* Runs in node.js and all browsers. +* Registered as a [ComponentJS](https://github.com/component/component) [component](https://github.com/component/component/wiki/Components) ('broofa/node-uuid'). +* Cryptographically strong random # generation on supporting platforms +* 1.1K minified and gzip'ed (Want something smaller? Check this [crazy shit](https://gist.github.com/982883) out! ) +* [Annotated source code](http://broofa.github.com/node-uuid/docs/uuid.html) +* Comes with a Command Line Interface for generating uuids on the command line + +## Getting Started + +Install it in your browser: + +```html + +``` + +Or in node.js: + +``` +npm install node-uuid +``` + +```javascript +var uuid = require('node-uuid'); +``` + +Then create some ids ... + +```javascript +// Generate a v1 (time-based) id +uuid.v1(); // -> '6c84fb90-12c4-11e1-840d-7b25c5ee775a' + +// Generate a v4 (random) id +uuid.v4(); // -> '110ec58a-a0f2-4ac4-8393-c866d813b8d1' +``` + +## API + +### uuid.v1([`options` [, `buffer` [, `offset`]]]) + +Generate and return a RFC4122 v1 (timestamp-based) UUID. + +* `options` - (Object) Optional uuid state to apply. Properties may include: + + * `node` - (Array) Node id as Array of 6 bytes (per 4.1.6). Default: Randomly generated ID. See note 1. + * `clockseq` - (Number between 0 - 0x3fff) RFC clock sequence. Default: An internally maintained clockseq is used. + * `msecs` - (Number | Date) Time in milliseconds since unix Epoch. Default: The current time is used. + * `nsecs` - (Number between 0-9999) additional time, in 100-nanosecond units. Ignored if `msecs` is unspecified. Default: internal uuid counter is used, as per 4.2.1.2. + +* `buffer` - (Array | Buffer) Array or buffer where UUID bytes are to be written. +* `offset` - (Number) Starting index in `buffer` at which to begin writing. + +Returns `buffer`, if specified, otherwise the string form of the UUID + +Notes: + +1. The randomly generated node id is only guaranteed to stay constant for the lifetime of the current JS runtime. (Future versions of this module may use persistent storage mechanisms to extend this guarantee.) + +Example: Generate string UUID with fully-specified options + +```javascript +uuid.v1({ + node: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab], + clockseq: 0x1234, + msecs: new Date('2011-11-01').getTime(), + nsecs: 5678 +}); // -> "710b962e-041c-11e1-9234-0123456789ab" +``` + +Example: In-place generation of two binary IDs + +```javascript +// Generate two ids in an array +var arr = new Array(32); // -> [] +uuid.v1(null, arr, 0); // -> [02 a2 ce 90 14 32 11 e1 85 58 0b 48 8e 4f c1 15] +uuid.v1(null, arr, 16); // -> [02 a2 ce 90 14 32 11 e1 85 58 0b 48 8e 4f c1 15 02 a3 1c b0 14 32 11 e1 85 58 0b 48 8e 4f c1 15] + +// Optionally use uuid.unparse() to get stringify the ids +uuid.unparse(buffer); // -> '02a2ce90-1432-11e1-8558-0b488e4fc115' +uuid.unparse(buffer, 16) // -> '02a31cb0-1432-11e1-8558-0b488e4fc115' +``` + +### uuid.v4([`options` [, `buffer` [, `offset`]]]) + +Generate and return a RFC4122 v4 UUID. + +* `options` - (Object) Optional uuid state to apply. Properties may include: + + * `random` - (Number[16]) Array of 16 numbers (0-255) to use in place of randomly generated values + * `rng` - (Function) Random # generator to use. Set to one of the built-in generators - `uuid.mathRNG` (all platforms), `uuid.nodeRNG` (node.js only), `uuid.whatwgRNG` (WebKit only) - or a custom function that returns an array[16] of byte values. + +* `buffer` - (Array | Buffer) Array or buffer where UUID bytes are to be written. +* `offset` - (Number) Starting index in `buffer` at which to begin writing. + +Returns `buffer`, if specified, otherwise the string form of the UUID + +Example: Generate string UUID with fully-specified options + +```javascript +uuid.v4({ + random: [ + 0x10, 0x91, 0x56, 0xbe, 0xc4, 0xfb, 0xc1, 0xea, + 0x71, 0xb4, 0xef, 0xe1, 0x67, 0x1c, 0x58, 0x36 + ] +}); +// -> "109156be-c4fb-41ea-b1b4-efe1671c5836" +``` + +Example: Generate two IDs in a single buffer + +```javascript +var buffer = new Array(32); // (or 'new Buffer' in node.js) +uuid.v4(null, buffer, 0); +uuid.v4(null, buffer, 16); +``` + +### uuid.parse(id[, buffer[, offset]]) +### uuid.unparse(buffer[, offset]) + +Parse and unparse UUIDs + + * `id` - (String) UUID(-like) string + * `buffer` - (Array | Buffer) Array or buffer where UUID bytes are to be written. Default: A new Array or Buffer is used + * `offset` - (Number) Starting index in `buffer` at which to begin writing. Default: 0 + +Example parsing and unparsing a UUID string + +```javascript +var bytes = uuid.parse('797ff043-11eb-11e1-80d6-510998755d10'); // -> +var string = uuid.unparse(bytes); // -> '797ff043-11eb-11e1-80d6-510998755d10' +``` + +### uuid.noConflict() + +(Browsers only) Set `uuid` property back to it's previous value. + +Returns the node-uuid object. + +Example: + +```javascript +var myUuid = uuid.noConflict(); +myUuid.v1(); // -> '6c84fb90-12c4-11e1-840d-7b25c5ee775a' +``` + +## Deprecated APIs + +Support for the following v1.2 APIs is available in v1.3, but is deprecated and will be removed in the next major version. + +### uuid([format [, buffer [, offset]]]) + +uuid() has become uuid.v4(), and the `format` argument is now implicit in the `buffer` argument. (i.e. if you specify a buffer, the format is assumed to be binary). + +### uuid.BufferClass + +The class of container created when generating binary uuid data if no buffer argument is specified. This is expected to go away, with no replacement API. + +## Command Line Interface + +To use the executable, it's probably best to install this library globally. + +`npm install -g node-uuid` + +Usage: + +``` +USAGE: uuid [version] [options] + + +options: + +--help Display this message and exit +``` + +`version` must be an RFC4122 version that is supported by this library, which is currently version 1 and version 4 (denoted by "v1" and "v4", respectively). `version` defaults to version 4 when not supplied. + +### Examples + +``` +> uuid +3a91f950-dec8-4688-ba14-5b7bbfc7a563 +``` + +``` +> uuid v1 +9d0b43e0-7696-11e3-964b-250efa37a98e +``` + +``` +> uuid v4 +6790ac7c-24ac-4f98-8464-42f6d98a53ae +``` + +## Testing + +In node.js + +``` +npm test +``` + +In Browser + +``` +open test/test.html +``` + +### Benchmarking + +Requires node.js + +``` +npm install uuid uuid-js +node benchmark/benchmark.js +``` + +For a more complete discussion of node-uuid performance, please see the `benchmark/README.md` file, and the [benchmark wiki](https://github.com/broofa/node-uuid/wiki/Benchmark) + +For browser performance [checkout the JSPerf tests](http://jsperf.com/node-uuid-performance). + +## Release notes + +### 1.4.0 + +* Improved module context detection +* Removed public RNG functions + +### 1.3.2 + +* Improve tests and handling of v1() options (Issue #24) +* Expose RNG option to allow for perf testing with different generators + +### 1.3.0 + +* Support for version 1 ids, thanks to [@ctavan](https://github.com/ctavan)! +* Support for node.js crypto API +* De-emphasizing performance in favor of a) cryptographic quality PRNGs where available and b) more manageable code diff --git a/sse-server/node_modules/node-uuid/benchmark/README.md b/sse-server/node_modules/node-uuid/benchmark/README.md new file mode 100644 index 0000000..aaeb2ea --- /dev/null +++ b/sse-server/node_modules/node-uuid/benchmark/README.md @@ -0,0 +1,53 @@ +# node-uuid Benchmarks + +### Results + +To see the results of our benchmarks visit https://github.com/broofa/node-uuid/wiki/Benchmark + +### Run them yourself + +node-uuid comes with some benchmarks to measure performance of generating UUIDs. These can be run using node.js. node-uuid is being benchmarked against some other uuid modules, that are available through npm namely `uuid` and `uuid-js`. + +To prepare and run the benchmark issue; + +``` +npm install uuid uuid-js +node benchmark/benchmark.js +``` + +You'll see an output like this one: + +``` +# v4 +nodeuuid.v4(): 854700 uuids/second +nodeuuid.v4('binary'): 788643 uuids/second +nodeuuid.v4('binary', buffer): 1336898 uuids/second +uuid(): 479386 uuids/second +uuid('binary'): 582072 uuids/second +uuidjs.create(4): 312304 uuids/second + +# v1 +nodeuuid.v1(): 938086 uuids/second +nodeuuid.v1('binary'): 683060 uuids/second +nodeuuid.v1('binary', buffer): 1644736 uuids/second +uuidjs.create(1): 190621 uuids/second +``` + +* The `uuid()` entries are for Nikhil Marathe's [uuid module](https://bitbucket.org/nikhilm/uuidjs) which is a wrapper around the native libuuid library. +* The `uuidjs()` entries are for Patrick Negri's [uuid-js module](https://github.com/pnegri/uuid-js) which is a pure javascript implementation based on [UUID.js](https://github.com/LiosK/UUID.js) by LiosK. + +If you want to get more reliable results you can run the benchmark multiple times and write the output into a log file: + +``` +for i in {0..9}; do node benchmark/benchmark.js >> benchmark/bench_0.4.12.log; done; +``` + +If you're interested in how performance varies between different node versions, you can issue the above command multiple times. + +You can then use the shell script `bench.sh` provided in this directory to calculate the averages over all benchmark runs and draw a nice plot: + +``` +(cd benchmark/ && ./bench.sh) +``` + +This assumes you have [gnuplot](http://www.gnuplot.info/) and [ImageMagick](http://www.imagemagick.org/) installed. You'll find a nice `bench.png` graph in the `benchmark/` directory then. diff --git a/sse-server/node_modules/node-uuid/benchmark/bench.gnu b/sse-server/node_modules/node-uuid/benchmark/bench.gnu new file mode 100644 index 0000000..a342fbb --- /dev/null +++ b/sse-server/node_modules/node-uuid/benchmark/bench.gnu @@ -0,0 +1,174 @@ +#!/opt/local/bin/gnuplot -persist +# +# +# G N U P L O T +# Version 4.4 patchlevel 3 +# last modified March 2011 +# System: Darwin 10.8.0 +# +# Copyright (C) 1986-1993, 1998, 2004, 2007-2010 +# Thomas Williams, Colin Kelley and many others +# +# gnuplot home: http://www.gnuplot.info +# faq, bugs, etc: type "help seeking-assistance" +# immediate help: type "help" +# plot window: hit 'h' +set terminal postscript eps noenhanced defaultplex \ + leveldefault color colortext \ + solid linewidth 1.2 butt noclip \ + palfuncparam 2000,0.003 \ + "Helvetica" 14 +set output 'bench.eps' +unset clip points +set clip one +unset clip two +set bar 1.000000 front +set border 31 front linetype -1 linewidth 1.000 +set xdata +set ydata +set zdata +set x2data +set y2data +set timefmt x "%d/%m/%y,%H:%M" +set timefmt y "%d/%m/%y,%H:%M" +set timefmt z "%d/%m/%y,%H:%M" +set timefmt x2 "%d/%m/%y,%H:%M" +set timefmt y2 "%d/%m/%y,%H:%M" +set timefmt cb "%d/%m/%y,%H:%M" +set boxwidth +set style fill empty border +set style rectangle back fc lt -3 fillstyle solid 1.00 border lt -1 +set style circle radius graph 0.02, first 0, 0 +set dummy x,y +set format x "% g" +set format y "% g" +set format x2 "% g" +set format y2 "% g" +set format z "% g" +set format cb "% g" +set angles radians +unset grid +set key title "" +set key outside left top horizontal Right noreverse enhanced autotitles columnhead nobox +set key noinvert samplen 4 spacing 1 width 0 height 0 +set key maxcolumns 2 maxrows 0 +unset label +unset arrow +set style increment default +unset style line +set style line 1 linetype 1 linewidth 2.000 pointtype 1 pointsize default pointinterval 0 +unset style arrow +set style histogram clustered gap 2 title offset character 0, 0, 0 +unset logscale +set offsets graph 0.05, 0.15, 0, 0 +set pointsize 1.5 +set pointintervalbox 1 +set encoding default +unset polar +unset parametric +unset decimalsign +set view 60, 30, 1, 1 +set samples 100, 100 +set isosamples 10, 10 +set surface +unset contour +set clabel '%8.3g' +set mapping cartesian +set datafile separator whitespace +unset hidden3d +set cntrparam order 4 +set cntrparam linear +set cntrparam levels auto 5 +set cntrparam points 5 +set size ratio 0 1,1 +set origin 0,0 +set style data points +set style function lines +set xzeroaxis linetype -2 linewidth 1.000 +set yzeroaxis linetype -2 linewidth 1.000 +set zzeroaxis linetype -2 linewidth 1.000 +set x2zeroaxis linetype -2 linewidth 1.000 +set y2zeroaxis linetype -2 linewidth 1.000 +set ticslevel 0.5 +set mxtics default +set mytics default +set mztics default +set mx2tics default +set my2tics default +set mcbtics default +set xtics border in scale 1,0.5 mirror norotate offset character 0, 0, 0 +set xtics norangelimit +set xtics () +set ytics border in scale 1,0.5 mirror norotate offset character 0, 0, 0 +set ytics autofreq norangelimit +set ztics border in scale 1,0.5 nomirror norotate offset character 0, 0, 0 +set ztics autofreq norangelimit +set nox2tics +set noy2tics +set cbtics border in scale 1,0.5 mirror norotate offset character 0, 0, 0 +set cbtics autofreq norangelimit +set title "" +set title offset character 0, 0, 0 font "" norotate +set timestamp bottom +set timestamp "" +set timestamp offset character 0, 0, 0 font "" norotate +set rrange [ * : * ] noreverse nowriteback # (currently [8.98847e+307:-8.98847e+307] ) +set autoscale rfixmin +set autoscale rfixmax +set trange [ * : * ] noreverse nowriteback # (currently [-5.00000:5.00000] ) +set autoscale tfixmin +set autoscale tfixmax +set urange [ * : * ] noreverse nowriteback # (currently [-10.0000:10.0000] ) +set autoscale ufixmin +set autoscale ufixmax +set vrange [ * : * ] noreverse nowriteback # (currently [-10.0000:10.0000] ) +set autoscale vfixmin +set autoscale vfixmax +set xlabel "" +set xlabel offset character 0, 0, 0 font "" textcolor lt -1 norotate +set x2label "" +set x2label offset character 0, 0, 0 font "" textcolor lt -1 norotate +set xrange [ * : * ] noreverse nowriteback # (currently [-0.150000:3.15000] ) +set autoscale xfixmin +set autoscale xfixmax +set x2range [ * : * ] noreverse nowriteback # (currently [0.00000:3.00000] ) +set autoscale x2fixmin +set autoscale x2fixmax +set ylabel "" +set ylabel offset character 0, 0, 0 font "" textcolor lt -1 rotate by -270 +set y2label "" +set y2label offset character 0, 0, 0 font "" textcolor lt -1 rotate by -270 +set yrange [ 0.00000 : 1.90000e+06 ] noreverse nowriteback # (currently [:] ) +set autoscale yfixmin +set autoscale yfixmax +set y2range [ * : * ] noreverse nowriteback # (currently [0.00000:1.90000e+06] ) +set autoscale y2fixmin +set autoscale y2fixmax +set zlabel "" +set zlabel offset character 0, 0, 0 font "" textcolor lt -1 norotate +set zrange [ * : * ] noreverse nowriteback # (currently [-10.0000:10.0000] ) +set autoscale zfixmin +set autoscale zfixmax +set cblabel "" +set cblabel offset character 0, 0, 0 font "" textcolor lt -1 rotate by -270 +set cbrange [ * : * ] noreverse nowriteback # (currently [8.98847e+307:-8.98847e+307] ) +set autoscale cbfixmin +set autoscale cbfixmax +set zero 1e-08 +set lmargin -1 +set bmargin -1 +set rmargin -1 +set tmargin -1 +set pm3d explicit at s +set pm3d scansautomatic +set pm3d interpolate 1,1 flush begin noftriangles nohidden3d corners2color mean +set palette positive nops_allcF maxcolors 0 gamma 1.5 color model RGB +set palette rgbformulae 7, 5, 15 +set colorbox default +set colorbox vertical origin screen 0.9, 0.2, 0 size screen 0.05, 0.6, 0 front bdefault +set loadpath +set fontpath +set fit noerrorvariables +GNUTERM = "aqua" +plot 'bench_results.txt' using 2:xticlabel(1) w lp lw 2, '' using 3:xticlabel(1) w lp lw 2, '' using 4:xticlabel(1) w lp lw 2, '' using 5:xticlabel(1) w lp lw 2, '' using 6:xticlabel(1) w lp lw 2, '' using 7:xticlabel(1) w lp lw 2, '' using 8:xticlabel(1) w lp lw 2, '' using 9:xticlabel(1) w lp lw 2 +# EOF diff --git a/sse-server/node_modules/node-uuid/benchmark/bench.sh b/sse-server/node_modules/node-uuid/benchmark/bench.sh new file mode 100755 index 0000000..d870a0c --- /dev/null +++ b/sse-server/node_modules/node-uuid/benchmark/bench.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# for a given node version run: +# for i in {0..9}; do node benchmark.js >> bench_0.6.2.log; done; + +PATTERNS=('nodeuuid.v1()' "nodeuuid.v1('binary'," 'nodeuuid.v4()' "nodeuuid.v4('binary'," "uuid()" "uuid('binary')" 'uuidjs.create(1)' 'uuidjs.create(4)' '140byte') +FILES=(node_uuid_v1_string node_uuid_v1_buf node_uuid_v4_string node_uuid_v4_buf libuuid_v4_string libuuid_v4_binary uuidjs_v1_string uuidjs_v4_string 140byte_es) +INDICES=(2 3 2 3 2 2 2 2 2) +VERSIONS=$( ls bench_*.log | sed -e 's/^bench_\([0-9\.]*\)\.log/\1/' | tr "\\n" " " ) +TMPJOIN="tmp_join" +OUTPUT="bench_results.txt" + +for I in ${!FILES[*]}; do + F=${FILES[$I]} + P=${PATTERNS[$I]} + INDEX=${INDICES[$I]} + echo "version $F" > $F + for V in $VERSIONS; do + (VAL=$( grep "$P" bench_$V.log | LC_ALL=en_US awk '{ sum += $'$INDEX' } END { print sum/NR }' ); echo $V $VAL) >> $F + done + if [ $I == 0 ]; then + cat $F > $TMPJOIN + else + join $TMPJOIN $F > $OUTPUT + cp $OUTPUT $TMPJOIN + fi + rm $F +done + +rm $TMPJOIN + +gnuplot bench.gnu +convert -density 200 -resize 800x560 -flatten bench.eps bench.png +rm bench.eps diff --git a/sse-server/node_modules/node-uuid/benchmark/benchmark-native.c b/sse-server/node_modules/node-uuid/benchmark/benchmark-native.c new file mode 100644 index 0000000..dbfc75f --- /dev/null +++ b/sse-server/node_modules/node-uuid/benchmark/benchmark-native.c @@ -0,0 +1,34 @@ +/* +Test performance of native C UUID generation + +To Compile: cc -luuid benchmark-native.c -o benchmark-native +*/ + +#include +#include +#include +#include + +int main() { + uuid_t myid; + char buf[36+1]; + int i; + struct timeval t; + double start, finish; + + gettimeofday(&t, NULL); + start = t.tv_sec + t.tv_usec/1e6; + + int n = 2e5; + for (i = 0; i < n; i++) { + uuid_generate(myid); + uuid_unparse(myid, buf); + } + + gettimeofday(&t, NULL); + finish = t.tv_sec + t.tv_usec/1e6; + double dur = finish - start; + + printf("%d uuids/sec", (int)(n/dur)); + return 0; +} diff --git a/sse-server/node_modules/node-uuid/benchmark/benchmark.js b/sse-server/node_modules/node-uuid/benchmark/benchmark.js new file mode 100644 index 0000000..40e6efb --- /dev/null +++ b/sse-server/node_modules/node-uuid/benchmark/benchmark.js @@ -0,0 +1,84 @@ +try { + var nodeuuid = require('../uuid'); +} catch (e) { + console.error('node-uuid require failed - skipping tests'); +} + +try { + var uuid = require('uuid'); +} catch (e) { + console.error('uuid require failed - skipping tests'); +} + +try { + var uuidjs = require('uuid-js'); +} catch (e) { + console.error('uuid-js require failed - skipping tests'); +} + +var N = 5e5; + +function rate(msg, t) { + console.log(msg + ': ' + + (N / (Date.now() - t) * 1e3 | 0) + + ' uuids/second'); +} + +console.log('# v4'); + +// node-uuid - string form +if (nodeuuid) { + for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v4(); + rate('nodeuuid.v4() - using node.js crypto RNG', t); + + for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v4({rng: nodeuuid.mathRNG}); + rate('nodeuuid.v4() - using Math.random() RNG', t); + + for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v4('binary'); + rate('nodeuuid.v4(\'binary\')', t); + + var buffer = new nodeuuid.BufferClass(16); + for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v4('binary', buffer); + rate('nodeuuid.v4(\'binary\', buffer)', t); +} + +// libuuid - string form +if (uuid) { + for (var i = 0, t = Date.now(); i < N; i++) uuid(); + rate('uuid()', t); + + for (var i = 0, t = Date.now(); i < N; i++) uuid('binary'); + rate('uuid(\'binary\')', t); +} + +// uuid-js - string form +if (uuidjs) { + for (var i = 0, t = Date.now(); i < N; i++) uuidjs.create(4); + rate('uuidjs.create(4)', t); +} + +// 140byte.es +for (var i = 0, t = Date.now(); i < N; i++) 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,function(s,r){r=Math.random()*16|0;return (s=='x'?r:r&0x3|0x8).toString(16)}); +rate('140byte.es_v4', t); + +console.log(''); +console.log('# v1'); + +// node-uuid - v1 string form +if (nodeuuid) { + for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v1(); + rate('nodeuuid.v1()', t); + + for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v1('binary'); + rate('nodeuuid.v1(\'binary\')', t); + + var buffer = new nodeuuid.BufferClass(16); + for (var i = 0, t = Date.now(); i < N; i++) nodeuuid.v1('binary', buffer); + rate('nodeuuid.v1(\'binary\', buffer)', t); +} + +// uuid-js - v1 string form +if (uuidjs) { + for (var i = 0, t = Date.now(); i < N; i++) uuidjs.create(1); + rate('uuidjs.create(1)', t); +} diff --git a/sse-server/node_modules/node-uuid/bin/uuid b/sse-server/node_modules/node-uuid/bin/uuid new file mode 100755 index 0000000..f732e99 --- /dev/null +++ b/sse-server/node_modules/node-uuid/bin/uuid @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +var path = require('path'); +var uuid = require(path.join(__dirname, '..')); + +var arg = process.argv[2]; + +if ('--help' === arg) { + console.log('\n USAGE: uuid [version] [options]\n\n'); + console.log(' options:\n'); + console.log(' --help Display this message and exit\n'); + process.exit(0); +} + +if (null == arg) { + console.log(uuid()); + process.exit(0); +} + +if ('v1' !== arg && 'v4' !== arg) { + console.error('Version must be RFC4122 version 1 or version 4, denoted as "v1" or "v4"'); + process.exit(1); +} + +console.log(uuid[arg]()); +process.exit(0); diff --git a/sse-server/node_modules/node-uuid/component.json b/sse-server/node_modules/node-uuid/component.json new file mode 100644 index 0000000..ace2134 --- /dev/null +++ b/sse-server/node_modules/node-uuid/component.json @@ -0,0 +1,18 @@ +{ + "name": "node-uuid", + "repo": "broofa/node-uuid", + "description": "Rigorous implementation of RFC4122 (v1 and v4) UUIDs.", + "version": "1.4.0", + "author": "Robert Kieffer ", + "contributors": [ + {"name": "Christoph Tavan ", "github": "https://github.com/ctavan"} + ], + "keywords": ["uuid", "guid", "rfc4122"], + "dependencies": {}, + "development": {}, + "main": "uuid.js", + "scripts": [ + "uuid.js" + ], + "license": "MIT" +} \ No newline at end of file diff --git a/sse-server/node_modules/node-uuid/package.json b/sse-server/node_modules/node-uuid/package.json new file mode 100644 index 0000000..c378347 --- /dev/null +++ b/sse-server/node_modules/node-uuid/package.json @@ -0,0 +1,26 @@ +{ + "name" : "node-uuid", + "description" : "Rigorous implementation of RFC4122 (v1 and v4) UUIDs.", + "url" : "http://github.com/broofa/node-uuid", + "keywords" : ["uuid", "guid", "rfc4122"], + "author" : "Robert Kieffer ", + "contributors" : [ + {"name": "Christoph Tavan ", "github": "https://github.com/ctavan"} + ], + "bin": { + "uuid": "./bin/uuid" + }, + "scripts": { + "test": "node test/test.js" + }, + "lib" : ".", + "main" : "./uuid.js", + "repository" : { "type" : "git", "url" : "https://github.com/broofa/node-uuid.git" }, + "version" : "1.4.2", + "licenses": [ + { + "type": "MIT", + "url": "https://raw.github.com/broofa/node-uuid/master/LICENSE.md" + } + ] +} diff --git a/sse-server/node_modules/node-uuid/test/compare_v1.js b/sse-server/node_modules/node-uuid/test/compare_v1.js new file mode 100644 index 0000000..05af822 --- /dev/null +++ b/sse-server/node_modules/node-uuid/test/compare_v1.js @@ -0,0 +1,63 @@ +var assert = require('assert'), + nodeuuid = require('../uuid'), + uuidjs = require('uuid-js'), + libuuid = require('uuid').generate, + util = require('util'), + exec = require('child_process').exec, + os = require('os'); + +// On Mac Os X / macports there's only the ossp-uuid package that provides uuid +// On Linux there's uuid-runtime which provides uuidgen +var uuidCmd = os.type() === 'Darwin' ? 'uuid -1' : 'uuidgen -t'; + +function compare(ids) { + console.log(ids); + for (var i = 0; i < ids.length; i++) { + var id = ids[i].split('-'); + id = [id[2], id[1], id[0]].join(''); + ids[i] = id; + } + var sorted = ([].concat(ids)).sort(); + + if (sorted.toString() !== ids.toString()) { + console.log('Warning: sorted !== ids'); + } else { + console.log('everything in order!'); + } +} + +// Test time order of v1 uuids +var ids = []; +while (ids.length < 10e3) ids.push(nodeuuid.v1()); + +var max = 10; +console.log('node-uuid:'); +ids = []; +for (var i = 0; i < max; i++) ids.push(nodeuuid.v1()); +compare(ids); + +console.log(''); +console.log('uuidjs:'); +ids = []; +for (var i = 0; i < max; i++) ids.push(uuidjs.create(1).toString()); +compare(ids); + +console.log(''); +console.log('libuuid:'); +ids = []; +var count = 0; +var last = function() { + compare(ids); +} +var cb = function(err, stdout, stderr) { + ids.push(stdout.substring(0, stdout.length-1)); + count++; + if (count < max) { + return next(); + } + last(); +}; +var next = function() { + exec(uuidCmd, cb); +}; +next(); diff --git a/sse-server/node_modules/node-uuid/test/test.html b/sse-server/node_modules/node-uuid/test/test.html new file mode 100644 index 0000000..d80326e --- /dev/null +++ b/sse-server/node_modules/node-uuid/test/test.html @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/sse-server/node_modules/node-uuid/test/test.js b/sse-server/node_modules/node-uuid/test/test.js new file mode 100644 index 0000000..2469225 --- /dev/null +++ b/sse-server/node_modules/node-uuid/test/test.js @@ -0,0 +1,228 @@ +if (!this.uuid) { + // node.js + uuid = require('../uuid'); +} + +// +// x-platform log/assert shims +// + +function _log(msg, type) { + type = type || 'log'; + + if (typeof(document) != 'undefined') { + document.write('
' + msg.replace(/\n/g, '
') + '
'); + } + if (typeof(console) != 'undefined') { + var color = { + log: '\033[39m', + warn: '\033[33m', + error: '\033[31m' + }; + console[type](color[type] + msg + color.log); + } +} + +function log(msg) {_log(msg, 'log');} +function warn(msg) {_log(msg, 'warn');} +function error(msg) {_log(msg, 'error');} + +function assert(res, msg) { + if (!res) { + error('FAIL: ' + msg); + } else { + log('Pass: ' + msg); + } +} + +// +// Unit tests +// + +// Verify ordering of v1 ids created with explicit times +var TIME = 1321644961388; // 2011-11-18 11:36:01.388-08:00 + +function compare(name, ids) { + ids = ids.map(function(id) { + return id.split('-').reverse().join('-'); + }).sort(); + var sorted = ([].concat(ids)).sort(); + + assert(sorted.toString() == ids.toString(), name + ' have expected order'); +} + +// Verify ordering of v1 ids created using default behavior +compare('uuids with current time', [ + uuid.v1(), + uuid.v1(), + uuid.v1(), + uuid.v1(), + uuid.v1() +]); + +// Verify ordering of v1 ids created with explicit times +compare('uuids with time option', [ + uuid.v1({msecs: TIME - 10*3600*1000}), + uuid.v1({msecs: TIME - 1}), + uuid.v1({msecs: TIME}), + uuid.v1({msecs: TIME + 1}), + uuid.v1({msecs: TIME + 28*24*3600*1000}) +]); + +assert( + uuid.v1({msecs: TIME}) != uuid.v1({msecs: TIME}), + 'IDs created at same msec are different' +); + +// Verify throw if too many ids created +var thrown = false; +try { + uuid.v1({msecs: TIME, nsecs: 10000}); +} catch (e) { + thrown = true; +} +assert(thrown, 'Exception thrown when > 10K ids created in 1 ms'); + +// Verify clock regression bumps clockseq +var uidt = uuid.v1({msecs: TIME}); +var uidtb = uuid.v1({msecs: TIME - 1}); +assert( + parseInt(uidtb.split('-')[3], 16) - parseInt(uidt.split('-')[3], 16) === 1, + 'Clock regression by msec increments the clockseq' +); + +// Verify clock regression bumps clockseq +var uidtn = uuid.v1({msecs: TIME, nsecs: 10}); +var uidtnb = uuid.v1({msecs: TIME, nsecs: 9}); +assert( + parseInt(uidtnb.split('-')[3], 16) - parseInt(uidtn.split('-')[3], 16) === 1, + 'Clock regression by nsec increments the clockseq' +); + +// Verify explicit options produce expected id +var id = uuid.v1({ + msecs: 1321651533573, + nsecs: 5432, + clockseq: 0x385c, + node: [ 0x61, 0xcd, 0x3c, 0xbb, 0x32, 0x10 ] +}); +assert(id == 'd9428888-122b-11e1-b85c-61cd3cbb3210', 'Explicit options produce expected id'); + +// Verify adjacent ids across a msec boundary are 1 time unit apart +var u0 = uuid.v1({msecs: TIME, nsecs: 9999}); +var u1 = uuid.v1({msecs: TIME + 1, nsecs: 0}); + +var before = u0.split('-')[0], after = u1.split('-')[0]; +var dt = parseInt(after, 16) - parseInt(before, 16); +assert(dt === 1, 'Ids spanning 1ms boundary are 100ns apart'); + +// +// Test parse/unparse +// + +id = '00112233445566778899aabbccddeeff'; +assert(uuid.unparse(uuid.parse(id.substr(0,10))) == + '00112233-4400-0000-0000-000000000000', 'Short parse'); +assert(uuid.unparse(uuid.parse('(this is the uuid -> ' + id + id)) == + '00112233-4455-6677-8899-aabbccddeeff', 'Dirty parse'); + +// +// Perf tests +// + +var generators = { + v1: uuid.v1, + v4: uuid.v4 +}; + +var UUID_FORMAT = { + v1: /[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i, + v4: /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i +}; + +var N = 1e4; + +// Get %'age an actual value differs from the ideal value +function divergence(actual, ideal) { + return Math.round(100*100*(actual - ideal)/ideal)/100; +} + +function rate(msg, t) { + log(msg + ': ' + (N / (Date.now() - t) * 1e3 | 0) + ' uuids\/second'); +} + +for (var version in generators) { + var counts = {}, max = 0; + var generator = generators[version]; + var format = UUID_FORMAT[version]; + + log('\nSanity check ' + N + ' ' + version + ' uuids'); + for (var i = 0, ok = 0; i < N; i++) { + id = generator(); + if (!format.test(id)) { + throw Error(id + ' is not a valid UUID string'); + } + + if (id != uuid.unparse(uuid.parse(id))) { + assert(fail, id + ' is not a valid id'); + } + + // Count digits for our randomness check + if (version == 'v4') { + var digits = id.replace(/-/g, '').split(''); + for (var j = digits.length-1; j >= 0; j--) { + var c = digits[j]; + max = Math.max(max, counts[c] = (counts[c] || 0) + 1); + } + } + } + + // Check randomness for v4 UUIDs + if (version == 'v4') { + // Limit that we get worried about randomness. (Purely empirical choice, this!) + var limit = 2*100*Math.sqrt(1/N); + + log('\nChecking v4 randomness. Distribution of Hex Digits (% deviation from ideal)'); + + for (var i = 0; i < 16; i++) { + var c = i.toString(16); + var bar = '', n = counts[c], p = Math.round(n/max*100|0); + + // 1-3,5-8, and D-F: 1:16 odds over 30 digits + var ideal = N*30/16; + if (i == 4) { + // 4: 1:1 odds on 1 digit, plus 1:16 odds on 30 digits + ideal = N*(1 + 30/16); + } else if (i >= 8 && i <= 11) { + // 8-B: 1:4 odds on 1 digit, plus 1:16 odds on 30 digits + ideal = N*(1/4 + 30/16); + } else { + // Otherwise: 1:16 odds on 30 digits + ideal = N*30/16; + } + var d = divergence(n, ideal); + + // Draw bar using UTF squares (just for grins) + var s = n/max*50 | 0; + while (s--) bar += '='; + + assert(Math.abs(d) < limit, c + ' |' + bar + '| ' + counts[c] + ' (' + d + '% < ' + limit + '%)'); + } + } +} + +// Perf tests +for (var version in generators) { + log('\nPerformance testing ' + version + ' UUIDs'); + var generator = generators[version]; + var buf = new uuid.BufferClass(16); + + for (var i = 0, t = Date.now(); i < N; i++) generator(); + rate('uuid.' + version + '()', t); + + for (var i = 0, t = Date.now(); i < N; i++) generator('binary'); + rate('uuid.' + version + '(\'binary\')', t); + + for (var i = 0, t = Date.now(); i < N; i++) generator('binary', buf); + rate('uuid.' + version + '(\'binary\', buffer)', t); +} diff --git a/sse-server/node_modules/node-uuid/uuid.js b/sse-server/node_modules/node-uuid/uuid.js new file mode 100644 index 0000000..5e2257f --- /dev/null +++ b/sse-server/node_modules/node-uuid/uuid.js @@ -0,0 +1,245 @@ +// uuid.js +// +// Copyright (c) 2010-2012 Robert Kieffer +// MIT License - http://opensource.org/licenses/mit-license.php + +(function() { + var _global = this; + + // Unique ID creation requires a high quality random # generator. We feature + // detect to determine the best RNG source, normalizing to a function that + // returns 128-bits of randomness, since that's what's usually required + var _rng; + + // Node.js crypto-based RNG - http://nodejs.org/docs/v0.6.2/api/crypto.html + // + // Moderately fast, high quality + if (typeof(_global.require) == 'function') { + try { + var _rb = _global.require('crypto').randomBytes; + _rng = _rb && function() {return _rb(16);}; + } catch(e) {} + } + + if (!_rng && _global.crypto && crypto.getRandomValues) { + // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto + // + // Moderately fast, high quality + var _rnds8 = new Uint8Array(16); + _rng = function whatwgRNG() { + crypto.getRandomValues(_rnds8); + return _rnds8; + }; + } + + if (!_rng) { + // Math.random()-based (RNG) + // + // If all else fails, use Math.random(). It's fast, but is of unspecified + // quality. + var _rnds = new Array(16); + _rng = function() { + for (var i = 0, r; i < 16; i++) { + if ((i & 0x03) === 0) r = Math.random() * 0x100000000; + _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff; + } + + return _rnds; + }; + } + + // Buffer class to use + var BufferClass = typeof(_global.Buffer) == 'function' ? _global.Buffer : Array; + + // Maps for number <-> hex string conversion + var _byteToHex = []; + var _hexToByte = {}; + for (var i = 0; i < 256; i++) { + _byteToHex[i] = (i + 0x100).toString(16).substr(1); + _hexToByte[_byteToHex[i]] = i; + } + + // **`parse()` - Parse a UUID into it's component bytes** + function parse(s, buf, offset) { + var i = (buf && offset) || 0, ii = 0; + + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { + if (ii < 16) { // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; + } + }); + + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } + + return buf; + } + + // **`unparse()` - Convert UUID byte array (ala parse()) into a string** + function unparse(buf, offset) { + var i = offset || 0, bth = _byteToHex; + return bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]]; + } + + // **`v1()` - Generate time-based UUID** + // + // Inspired by https://github.com/LiosK/UUID.js + // and http://docs.python.org/library/uuid.html + + // random #'s we need to init node and clockseq + var _seedBytes = _rng(); + + // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) + var _nodeId = [ + _seedBytes[0] | 0x01, + _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] + ]; + + // Per 4.2.2, randomize (14 bit) clockseq + var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; + + // Previous uuid creation time + var _lastMSecs = 0, _lastNSecs = 0; + + // See https://github.com/broofa/node-uuid for API details + function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; + + options = options || {}; + + var clockseq = options.clockseq != null ? options.clockseq : _clockseq; + + // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + var msecs = options.msecs != null ? options.msecs : new Date().getTime(); + + // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + var nsecs = options.nsecs != null ? options.nsecs : _lastNSecs + 1; + + // Time since last uuid creation (in msecs) + var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; + + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq == null) { + clockseq = clockseq + 1 & 0x3fff; + } + + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs == null) { + nsecs = 0; + } + + // Per 4.2.1.2 Throw error if too many uuids are requested + if (nsecs >= 10000) { + throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); + } + + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; + + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; + + // `time_low` + var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; + b[i++] = tl >>> 24 & 0xff; + b[i++] = tl >>> 16 & 0xff; + b[i++] = tl >>> 8 & 0xff; + b[i++] = tl & 0xff; + + // `time_mid` + var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; + b[i++] = tmh >>> 8 & 0xff; + b[i++] = tmh & 0xff; + + // `time_high_and_version` + b[i++] = tmh >>> 24 & 0xf | 0x10; // include version + b[i++] = tmh >>> 16 & 0xff; + + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 0x80; + + // `clock_seq_low` + b[i++] = clockseq & 0xff; + + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; + } + + return buf ? buf : unparse(b); + } + + // **`v4()` - Generate random UUID** + + // See https://github.com/broofa/node-uuid for API details + function v4(options, buf, offset) { + // Deprecated - 'format' argument, as supported in v1.2 + var i = buf && offset || 0; + + if (typeof(options) == 'string') { + buf = options == 'binary' ? new BufferClass(16) : null; + options = null; + } + options = options || {}; + + var rnds = options.random || (options.rng || _rng)(); + + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + rnds[6] = (rnds[6] & 0x0f) | 0x40; + rnds[8] = (rnds[8] & 0x3f) | 0x80; + + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } + } + + return buf || unparse(rnds); + } + + // Export public API + var uuid = v4; + uuid.v1 = v1; + uuid.v4 = v4; + uuid.parse = parse; + uuid.unparse = unparse; + uuid.BufferClass = BufferClass; + + if (typeof define === 'function' && define.amd) { + // Publish as AMD module + define(function() {return uuid;}); + } else if (typeof(module) != 'undefined' && module.exports) { + // Publish as node.js module + module.exports = uuid; + } else { + // Publish as global (in browsers) + var _previousRoot = _global.uuid; + + // **`noConflict()` - (browser only) to reset global 'uuid' var** + uuid.noConflict = function() { + _global.uuid = _previousRoot; + return uuid; + }; + + _global.uuid = uuid; + } +}).call(this); diff --git a/sse-server/package-lock.json b/sse-server/package-lock.json new file mode 100644 index 0000000..01cf8df --- /dev/null +++ b/sse-server/package-lock.json @@ -0,0 +1,24 @@ +{ + "name": "SSE-Example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "SSE-Example", + "version": "1.0.0", + "dependencies": { + "node-uuid": "1.4.2" + } + }, + "node_modules/node-uuid": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.2.tgz", + "integrity": "sha1-kH2z0Rt7aiz0+QX7cZnxSuc3m6A=", + "deprecated": "Use uuid module instead", + "bin": { + "uuid": "bin/uuid" + } + } + } +} diff --git a/sse-server/package.json b/sse-server/package.json new file mode 100644 index 0000000..18acdac --- /dev/null +++ b/sse-server/package.json @@ -0,0 +1,9 @@ +{ + "name": "SSE-Example", + "description": "Example for iOS EventSource implementation on Swift", + "author": "Andres Canal", + "version": "1.0.0", + "dependencies": { + "node-uuid": "1.4.2" + } +} diff --git a/sse-server/sse.js b/sse-server/sse.js new file mode 100644 index 0000000..ca60607 --- /dev/null +++ b/sse-server/sse.js @@ -0,0 +1,72 @@ +var http = require('http'); +var uuid = require('node-uuid'); + +var sendInterval = 3000; + +var userNames = ["John", "Monica", "Martin", "Rose", "James", "Tom"]; +var randomMessages = ["I'm fine", "I'll meet you in 10 minutes", "I'll call you back later", "I'm busy"]; + +var intervalID = null; + +function sendServerSendEvent(req, res) { + console.log("Connected") + res.writeHead(200, { + 'Content-Type' : 'text/event-stream', + 'Cache-Control' : 'no-cache', + 'Connection' : 'keep-alive' + }); + + res.write('\n'); + + intervalID = setInterval(function() { + var eventId = uuid.v1(); + var eventName = null; + var eventData = randomMessages[getRandom(0,randomMessages.length)] + + if (getRandom(0,2) == 0) { + eventName = "user-connected"; + eventData = userNames[getRandom(0,userNames.length)] + } + + console.log('id: ' + eventId); + console.log('event: ' + eventName); + console.log('data: ' + eventData + '\n'); + + writeEvent(res, eventId , eventName, eventData); + + }, sendInterval); +} + +function writeEvent(res, sseId, eventName, data) { + var payload = "" + + payload = 'id: ' + sseId + '\n'; + + if (eventName) { + payload += 'event: ' + eventName + '\n'; + } + + payload += 'data: ' + data + '\n\n'; + + res.write(payload); +} + +function getRandom(min, max) { + return Math.floor(Math.random() * (max - min) + min); +} + +http.createServer(function(req, res) { + if (req.headers.accept && req.headers.accept == 'text/event-stream' && req.url == '/sse'){ + + req.on('close', function(){ + clearInterval(intervalID); + }); + + sendServerSendEvent(req, res); + }else{ + res.writeHead(404); + res.end(); + } +}).listen(8080); + +console.log("Listening")