Repository: launchdarkly/swift-eventsource Branch: main Commit: f63736db6fe7 Files: 56 Total size: 135.8 KB Directory structure: gitextract_1la2044u/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── actions/ │ │ ├── build-docs/ │ │ │ └── action.yml │ │ ├── build-ios/ │ │ │ └── action.yml │ │ ├── build-macos/ │ │ │ └── action.yml │ │ ├── build-tvos/ │ │ │ └── action.yml │ │ ├── build-watchos/ │ │ │ └── action.yml │ │ ├── contract-tests/ │ │ │ └── action.yml │ │ ├── lint/ │ │ │ └── action.yml │ │ ├── publish/ │ │ │ └── action.yml │ │ ├── publish-docs/ │ │ │ └── action.yml │ │ ├── test-swiftpm/ │ │ │ └── action.yml │ │ └── update-versions/ │ │ └── action.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── ci.yml │ ├── lint-pr-title.yml │ ├── manual-publish-docs.yml │ ├── manual-publish.yml │ ├── release-please.yml │ └── stale.yml ├── .gitignore ├── .jazzy.yaml ├── .release-please-manifest.json ├── .swiftlint.yml ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── ContractTestService/ │ ├── .gitignore │ ├── Package.resolved │ ├── Package.swift │ ├── README.md │ └── Sources/ │ └── ContractTestService/ │ └── main.swift ├── LDSwiftEventSource.podspec ├── LDSwiftEventSource.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ └── contents.xcworkspacedata │ └── xcshareddata/ │ └── xcschemes/ │ └── LDSwiftEventSource.xcscheme ├── LICENSE.txt ├── Makefile ├── Package.swift ├── README.md ├── SECURITY.md ├── Source/ │ ├── .swiftlint.yml │ ├── EventParser.swift │ ├── Info.plist │ ├── LDSwiftEventSource.h │ ├── LDSwiftEventSource.swift │ ├── Logs.swift │ ├── Types.swift │ └── UTF8LineParser.swift ├── Tests/ │ ├── .swiftlint.yml │ ├── EventParserTests.swift │ ├── LDSwiftEventSourceTests.swift │ ├── MockHandler.swift │ ├── TestUtil.swift │ └── UTF8LineParserTests.swift └── release-please-config.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To reproduce** Steps to reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. **Logs** If applicable, add any log output related to your problem. **Library version** The version that you are using. **XCode and Swift version** For instance, XCode 11.5, Swift 5.1. **Platform the issue occurs on** iPhone, iPad, macOS, tvOS, or watchOS. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I would love to see the library [...does something new...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context about the feature request here. ================================================ FILE: .github/actions/build-docs/action.yml ================================================ name: Build Documentation description: 'Build Documentation.' runs: using: composite steps: - name: Install jazzy gem shell: bash run: gem install jazzy - name: Build Documentation shell: bash run: jazzy -o docs - name: Validate coverage shell: bash run: | FULLDOC=`jq '.warnings | length == 0' docs/undocumented.json` [ $FULLDOC == "true" ] ================================================ FILE: .github/actions/build-ios/action.yml ================================================ name: Build & Test iOS description: 'Build for iOS device and run tests on iOS Simulator.' inputs: ios-sim: description: 'iOS Simulator to use for testing' required: true runs: using: composite steps: # Workaround for intermittent macos-15 runner issue where simulator # runtimes aren't loaded. Listing devices forces the simulator service # to initialize. See https://github.com/actions/runner-images/issues/12948 - name: Prepare iOS Simulator runtime shell: bash run: xcrun simctl list devices available - name: Build Tests for iOS device shell: bash run: xcodebuild build-for-testing -scheme 'LDSwiftEventSource' -sdk iphoneos CODE_SIGN_IDENTITY= | xcpretty - name: Build & Test on iOS Simulator shell: bash run: xcodebuild test -scheme 'LDSwiftEventSource' -sdk iphonesimulator -destination '${{ inputs.ios-sim }}' CODE_SIGN_IDENTITY= | xcpretty ================================================ FILE: .github/actions/build-macos/action.yml ================================================ name: Build & Test macOS description: 'Build and test for macOS.' runs: using: composite steps: - name: Build & Test on macOS shell: bash run: xcodebuild test -scheme 'LDSwiftEventSource' -sdk macosx -destination 'platform=macOS' | xcpretty - name: Build for ARM64 macOS shell: bash run: xcodebuild build -scheme 'LDSwiftEventSource' -arch arm64e -sdk macosx | xcpretty ================================================ FILE: .github/actions/build-tvos/action.yml ================================================ name: Build & Test tvOS description: 'Build for tvOS device and run tests on tvOS Simulator.' runs: using: composite steps: # Workaround for intermittent macos-15 runner issue where simulator # runtimes aren't loaded. Listing devices forces the simulator service # to initialize. See https://github.com/actions/runner-images/issues/12948 - name: Prepare tvOS Simulator runtime shell: bash run: xcrun simctl list devices available - name: Build Tests for tvOS device shell: bash run: xcodebuild build-for-testing -scheme 'LDSwiftEventSource' -sdk appletvos CODE_SIGN_IDENTITY= | xcpretty - name: Build & Test on tvOS Simulator shell: bash run: xcodebuild test -scheme 'LDSwiftEventSource' -sdk appletvsimulator -destination 'platform=tvOS Simulator,name=Apple TV' | xcpretty ================================================ FILE: .github/actions/build-watchos/action.yml ================================================ name: Build watchOS description: 'Build for watchOS device and simulator.' runs: using: composite steps: # Workaround for intermittent macos-15 runner issue where simulator # runtimes aren't loaded. Listing devices forces the simulator service # to initialize. See https://github.com/actions/runner-images/issues/12948 - name: Prepare watchOS Simulator runtime shell: bash run: xcrun simctl list devices available - name: Build for watchOS simulator shell: bash run: xcodebuild build -scheme 'LDSwiftEventSource' -sdk watchsimulator | xcpretty - name: Build for watchOS device shell: bash run: xcodebuild build -scheme 'LDSwiftEventSource' -sdk watchos | xcpretty ================================================ FILE: .github/actions/contract-tests/action.yml ================================================ name: Contract Tests description: 'Build and run SDK contract tests.' inputs: token: description: 'GH token used to download SDK test harness.' required: true runs: using: composite steps: - name: Build contract test service shell: bash run: make build-contract-tests - name: Start contract test service shell: bash run: make start-contract-test-service-bg - name: Run contract tests uses: launchdarkly/gh-actions/actions/contract-tests@main with: repo: sse-contract-tests branch: main version: v2 token: ${{ inputs.token }} test_service_port: '8000' debug_logging: 'true' enable_persistence_tests: 'false' extra_params: "-skip 'basic parsing/large message in one chunk' -skip 'basic parsing/large message in two chunks'" ================================================ FILE: .github/actions/lint/action.yml ================================================ name: Lint description: 'Run podspec and swiftlint checks.' runs: using: composite steps: - name: Install swiftlint shell: bash run: brew install swiftlint - name: Install cocoapods shell: bash run: gem install cocoapods - name: Lint the podspec shell: bash # --quick skips building since dedicated build jobs already compile all platforms run: pod lib lint LDSwiftEventSource.podspec --allow-warnings --quick - name: Run swiftlint shell: bash run: swiftlint lint ================================================ FILE: .github/actions/publish/action.yml ================================================ name: Publish Package description: 'Publish the package to Cocoapods' inputs: dry_run: description: 'Is this a dry run. If so no package will be published.' required: true runs: using: composite steps: - name: Push to cocoapods if: ${{ inputs.dry_run == 'false' }} shell: bash run: pod trunk push LDSwiftEventSource.podspec --allow-warnings --verbose ================================================ FILE: .github/actions/publish-docs/action.yml ================================================ name: Publish Documentation description: 'Publish the documentation to GitHub pages' inputs: token: description: 'Token to use for publishing.' required: true runs: using: composite steps: - uses: launchdarkly/gh-actions/actions/publish-pages@publish-pages-v1.0.2 name: 'Publish to GitHub pages' with: docs_path: docs github_token: ${{ inputs.token }} ================================================ FILE: .github/actions/test-swiftpm/action.yml ================================================ name: Test SwiftPM description: 'Build and test using Swift Package Manager.' runs: using: composite steps: - name: Build & Test with SwiftPM shell: bash run: swift test -v 2>&1 | xcpretty ================================================ FILE: .github/actions/update-versions/action.yml ================================================ name: Update xcode project version numbers description: 'Update xcode project version numbers' inputs: branch: description: 'The branch to checkout and push updates to' required: true runs: using: composite steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.branch }} - name: Calculate version numbers id: version shell: bash run: | version=$(jq -r '."."' .release-please-manifest.json) major=$(echo "$version" | cut -f1 -d.) minor=$(echo "$version" | cut -f2 -d.) patch=$(echo "$version" | cut -f3 -d.) # 64 + version gives us a letter offset for the framework version. framework=$(echo $((major + 64)) | awk '{ printf("%c", $1) }') echo "major=${major}" >> "$GITHUB_OUTPUT" echo "minor=${minor}" >> "$GITHUB_OUTPUT" echo "patch=${patch}" >> "$GITHUB_OUTPUT" echo "framework=${framework}" >> "$GITHUB_OUTPUT" - name: Update other version numbers shell: bash run: | sed -i .bak -E \ -e 's/MARKETING_VERSION = [^;]+/MARKETING_VERSION = ${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }}.${{ steps.version.outputs.patch }}/' \ -e 's/DYLIB_CURRENT_VERSION = [^;]+/DYLIB_CURRENT_VERSION = ${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }}.${{ steps.version.outputs.patch }}/' \ -e 's/DYLIB_COMPATIBILITY_VERSION = [^;]+/DYLIB_COMPATIBILITY_VERSION = ${{ steps.version.outputs.major }}.0.0/' \ -e 's/FRAMEWORK_VERSION = .*/FRAMEWORK_VERSION = ${{ steps.version.outputs.framework }};/' \ LDSwiftEventSource.xcodeproj/project.pbxproj sed -i .bak -E \ -e "s/pod 'LDSwiftEventSource', '~> [0-9]+.[0-9]+'/pod 'LDSwiftEventSource', '~> ${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }}'/" \ -e "s/github \"LaunchDarkly\/swift-eventsource\" ~> [0-9]+.[0-9]+/github \"LaunchDarkly\/swift-eventsource\" ~> ${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }}/" README.md rm -f LDSwiftEventSource.xcodeproj/project.pbxproj.bak README.md.bak if [ $(git status --porcelain | wc -l) -gt 0 ]; then git config --global user.name 'LaunchDarklyReleaseBot' git config --global user.email 'LaunchDarklyReleaseBot@launchdarkly.com' git add LDSwiftEventSource.xcodeproj/project.pbxproj git add README.md git commit -m 'Updating generated project and readme files' git push fi ================================================ FILE: .github/pull_request_template.md ================================================ **Requirements** - [ ] I have added test coverage for new or changed functionality - [ ] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions **Related issues** Provide links to any issues in this repository or elsewhere relating to this pull request. **Describe the solution you've provided** Provide a clear and concise description of what you expect to happen. **Describe alternatives you've considered** Provide a clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context about the pull request here. ================================================ FILE: .github/workflows/ci.yml ================================================ name: Run CI on: push: branches: [ main ] paths-ignore: - '**.md' # Do not need to run CI for markdown changes. pull_request: branches: [ main ] paths-ignore: - '**.md' jobs: lint: runs-on: macos-15 steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd with: xcode-version: 16.4 - uses: ./.github/actions/lint build-ios: runs-on: macos-15 steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd with: xcode-version: 16.4 - uses: ./.github/actions/build-ios with: ios-sim: 'platform=iOS Simulator,name=iPhone 16' build-macos: runs-on: macos-15 steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd with: xcode-version: 16.4 - uses: ./.github/actions/build-macos build-tvos: runs-on: macos-15 steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd with: xcode-version: 16.4 - uses: ./.github/actions/build-tvos build-watchos: runs-on: macos-15 steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd with: xcode-version: 16.4 - uses: ./.github/actions/build-watchos test-swiftpm: runs-on: macos-15 steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd with: xcode-version: 16.4 - uses: ./.github/actions/test-swiftpm contract-tests: runs-on: macos-15 steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd with: xcode-version: 16.4 - uses: ./.github/actions/contract-tests with: token: ${{ secrets.GITHUB_TOKEN }} build-docs: runs-on: macos-15 steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd with: xcode-version: 16.4 - uses: ./.github/actions/build-docs linux-build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: swift-version: - 5.7 - 5.8 - 5.9 container: swift:${{ matrix.swift-version }} steps: - uses: actions/checkout@v4 - name: Build and test run: swift test --enable-test-discovery windows-build: name: Windows - Swift 6.1 runs-on: windows-latest steps: - uses: actions/checkout@v4 - name: Install Swift uses: compnerd/gha-setup-swift@cd348eb89f2f450b0664c07fb1cb66880addf17d with: branch: swift-6.1-release tag: 6.1-RELEASE - name: Build and test run: swift test ================================================ FILE: .github/workflows/lint-pr-title.yml ================================================ name: Lint PR title on: pull_request_target: types: - opened - edited - synchronize jobs: lint-pr-title: uses: launchdarkly/gh-actions/.github/workflows/lint-pr-title.yml@main ================================================ FILE: .github/workflows/manual-publish-docs.yml ================================================ on: workflow_dispatch: name: Publish Documentation jobs: build-publish: runs-on: macos-15 permissions: id-token: write # Needed if using OIDC to get release secrets. contents: write # Needed in this case to write github pages. steps: - uses: actions/checkout@v4 - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd with: xcode-version: 16.4 - uses: ./.github/actions/lint - uses: ./.github/actions/build-ios with: ios-sim: 'platform=iOS Simulator,name=iPhone 16' - uses: ./.github/actions/build-macos - uses: ./.github/actions/build-tvos - uses: ./.github/actions/build-watchos - uses: ./.github/actions/test-swiftpm - uses: ./.github/actions/contract-tests with: token: ${{ secrets.GITHUB_TOKEN }} - uses: ./.github/actions/build-docs - uses: ./.github/actions/publish-docs with: token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/manual-publish.yml ================================================ name: Publish Package on: workflow_dispatch: inputs: dry_run: description: 'Is this a dry run. If so no package will be published.' type: boolean required: true jobs: build-publish: runs-on: macos-15 # Needed to get tokens during publishing. permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 name: 'Get Cocoapods token' with: aws_assume_role: ${{ vars.AWS_ROLE_ARN }} ssm_parameter_pairs: '/production/common/releasing/cocoapods/token = COCOAPODS_TRUNK_TOKEN' - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd with: xcode-version: 16.4 - uses: ./.github/actions/lint - uses: ./.github/actions/build-ios with: ios-sim: 'platform=iOS Simulator,name=iPhone 16' - uses: ./.github/actions/build-macos - uses: ./.github/actions/build-tvos - uses: ./.github/actions/build-watchos - uses: ./.github/actions/test-swiftpm - uses: ./.github/actions/contract-tests with: token: ${{ secrets.GITHUB_TOKEN }} - uses: ./.github/actions/publish with: dry_run: ${{ inputs.dry_run }} ================================================ FILE: .github/workflows/release-please.yml ================================================ name: Run Release Please on: push: branches: - main jobs: release-package: runs-on: macos-15 permissions: id-token: write # Needed if using OIDC to get release secrets. contents: write # Contents and pull-requests are for release-please to make releases. pull-requests: write steps: - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 id: release with: target-branch: ${{ github.ref_name }} - uses: actions/checkout@v4 with: fetch-depth: 0 # Full history is required for proper changelog generation # # This step runs and updates an existing PR # - uses: ./.github/actions/update-versions if: ${{ steps.release.outputs.prs_created == 'true' }} with: branch: ${{ fromJSON(steps.release.outputs.pr).headBranchName }} # # These remaining steps are ONLY run if a release was actually created # - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 if: ${{ steps.release.outputs.releases_created == 'true' }} name: 'Get Cocoapods token' with: aws_assume_role: ${{ vars.AWS_ROLE_ARN }} ssm_parameter_pairs: '/production/common/releasing/cocoapods/token = COCOAPODS_TRUNK_TOKEN' - uses: maxim-lobanov/setup-xcode@60606e260d2fc5762a71e64e74b2174e8ea3c8bd # 60606e260d2fc5762a71e64e74b2174e8ea3c8bd if: ${{ steps.release.outputs.releases_created == 'true' }} with: xcode-version: 16.4 - uses: ./.github/actions/lint if: ${{ steps.release.outputs.releases_created == 'true' }} - uses: ./.github/actions/build-ios if: ${{ steps.release.outputs.releases_created == 'true' }} with: ios-sim: 'platform=iOS Simulator,name=iPhone 16' - uses: ./.github/actions/build-macos if: ${{ steps.release.outputs.releases_created == 'true' }} - uses: ./.github/actions/build-tvos if: ${{ steps.release.outputs.releases_created == 'true' }} - uses: ./.github/actions/build-watchos if: ${{ steps.release.outputs.releases_created == 'true' }} - uses: ./.github/actions/test-swiftpm if: ${{ steps.release.outputs.releases_created == 'true' }} - uses: ./.github/actions/contract-tests if: ${{ steps.release.outputs.releases_created == 'true' }} with: token: ${{ secrets.GITHUB_TOKEN }} - uses: ./.github/actions/build-docs if: ${{ steps.release.outputs.releases_created == 'true' }} - uses: ./.github/actions/publish if: ${{ steps.release.outputs.releases_created == 'true' }} with: token: ${{secrets.GITHUB_TOKEN}} dry_run: false - uses: ./.github/actions/publish-docs if: ${{ steps.release.outputs.releases_created == 'true' }} with: token: ${{secrets.GITHUB_TOKEN}} ================================================ FILE: .github/workflows/stale.yml ================================================ name: "Close stale issues and PRs" on: workflow_dispatch: schedule: # Happen once per day at 1:30 AM - cron: "30 1 * * *" permissions: issues: write pull-requests: write jobs: sdk-close-stale: uses: launchdarkly/gh-actions/.github/workflows/sdk-stale.yml@main ================================================ FILE: .gitignore ================================================ *~ \#* .\#* .DS_Store /.build xcuserdata/ IDEWorkspaceChecks.plist .swiftpm /docs ================================================ FILE: .jazzy.yaml ================================================ module: LDSwiftEventSource author: LaunchDarkly author_url: https://launchdarkly.com github_url: https://github.com/launchdarkly/swift-eventsource clean: true swift_build_tool: spm readme: README.md documentation: - CHANGELOG.md - CONTRIBUTING.md - LICENSE.txt copyright: 'Copyright © 2020 Catamorphic Co.' ================================================ FILE: .release-please-manifest.json ================================================ { ".": "3.3.0" } ================================================ FILE: .swiftlint.yml ================================================ # See sub-configurations at `Source/.swiftlint.yml` and `Tests/.swiftlint.yml`. disabled_rules: - identifier_name - weak_delegate opt_in_rules: - anyobject_protocol - array_init - attributes - closure_body_length - closure_end_indentation - closure_spacing - collection_alignment - conditional_returns_on_newline - contains_over_filter_count - contains_over_filter_is_empty - contains_over_first_not_nil - contains_over_range_nil_comparison - discouraged_object_literal - discouraged_optional_boolean - discouraged_optional_collection - empty_collection_literal - empty_count - empty_string - empty_xctest_method - enum_case_associated_values_count - expiring_todo - explicit_init - explicit_self - extension_access_modifier - fallthrough - fatal_error_message - file_header - file_name_no_space - first_where - flatmap_over_map_reduce - function_default_parameter_at_end - identical_operands - implicit_return - joined_default_parameter - last_where - legacy_multiple - legacy_random - let_var_whitespace - literal_expression_end_indentation - missing_docs - modifier_order - no_grouping_extension - nslocalizedstring_key - nslocalizedstring_require_bundle - number_separator - object_literal - operator_usage_whitespace - optional_enum_case_matching - overridden_super_call - override_in_extension - pattern_matching_keywords - prefer_self_type_over_type_of_self - prefixed_toplevel_constant - private_action - private_outlet - prohibited_interface_builder - prohibited_super_call - raw_value_for_camel_cased_codable_enum - reduce_into - redundant_nil_coalescing - required_enum_case - single_test_class - sorted_first_last - static_operator - strict_fileprivate - strong_iboutlet - switch_case_on_newline - toggle_bool - trailing_closure - unavailable_function - unneeded_parentheses_in_closure_argument - unowned_variable_capture - untyped_error_in_catch - unused_declaration - unused_import - vertical_parameter_alignment_on_call - vertical_whitespace_closing_braces - vertical_whitespace_opening_braces - yoda_condition included: - Source - Tests reporter: "xcode" ================================================ FILE: CHANGELOG.md ================================================ # Change log All notable changes to the LaunchDarkly Swift EventSource library will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). ## [3.3.0](https://github.com/launchdarkly/swift-eventsource/compare/3.2.0...3.3.0) (2024-05-31) ### Features * adds ability to provide a OSLog instance via the Config.logger property ([c10ec29](https://github.com/launchdarkly/swift-eventsource/commit/c10ec2936e77959f828a041b71ea56e454e39ff2)) * adds ability to provide a OSLog instance via the Config.logger property ([#78](https://github.com/launchdarkly/swift-eventsource/issues/78)) ([2220929](https://github.com/launchdarkly/swift-eventsource/commit/2220929cbc98edd88e0e7d8b5ccb1f3992b4cf4f)) ## [3.2.0](https://github.com/launchdarkly/swift-eventsource/compare/3.1.1...3.2.0) (2023-12-29) ### Features * Add Compilation & Testing On Windows ([#68](https://github.com/launchdarkly/swift-eventsource/issues/68)) ([ac5f18c](https://github.com/launchdarkly/swift-eventsource/commit/ac5f18ccb5b197bbc9f37f9f799017a397eee43e)) ## [3.1.1] - 2023-06-12 ### Fixed: - Per the SSE spec, an HTTP 204 will now halt retry attempts by default. ## [3.1.0] - 2023-06-05 ### Changed: - Enforce TLS v1.2 as a required minimum. ### Fixed: - Fix re-entrancy issue with `start` command. (Thanks, [g-mark](https://github.com/launchdarkly/swift-eventsource/pull/56)!) ## [3.0.0] - 2022-10-06 ### Changed - Dropped support for older versions in accordance with the new [Xcode 14 release](https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes). ## [2.0.0] - 2022-08-29 ### Changed - The CI build now incorporates the cross-platform contract tests defined in https://github.com/launchdarkly/sse-contract-tests to ensure consistent test coverage across different LaunchDarkly SSE implementations. - Removed explicit typed package products. Thanks to @simba909 for the PR ([#48](https://github.com/launchdarkly/swift-eventsource/pull/48)). ## [1.3.1] - 2022-03-11 ### Fixed - Fixed a race condition that could cause a crash when `stop()` is called when there is a pending reconnection attempt. ## [1.3.0] - 2022-01-18 ### Added - Added the configuration option `urlSessionConfiguration` to `EventSource.Config` which allows setting the `URLSessionConfiguration` used by the `EventSource` to create `URLSession` instances. ### Fixed - Fixed a retain cycle issue when the stream connection is ended. - Removed deprecated `VALID_ARCHS` build setting from Xcode project. - Unterminated events will no longer be dispatched when the stream connection is dropped. - Stream events that set the `lastEventId` will now record the updated `lastEventId` even if the event does not generate a `MessageEvent`. - Empty stream "data" fields will now always record a newline to the resultant `MessageEvent` data. - Empty stream "event" fields will result in now result in the default "message" event type rather than an event type of "". ## [1.2.1] - 2021-02-10 ### Added - [SwiftLint](https://github.com/realm/SwiftLint) configuration. Linting will be automatically run as part of the build if [Mint](https://github.com/yonaskolb/Mint) is installed. - Support for building docs with [jazzy](https://github.com/realm/jazzy). These docs are available through [GitHub Pages](https://launchdarkly.github.io/swift-eventsource/). ### Fixed - Reconnection backoff was always reset if the previous successful connection was at least `backoffResetThreshold` prior to the scheduling of a reconnection attempt. The connection backoff has been corrected to not reset after the first reconnection attempt until the next successful connection. Thanks to @tomasf for the PR ([#14](https://github.com/launchdarkly/swift-eventsource/pull/14)). - On an `UnsuccessfulResponseError` the configured `connectionErrorHandler` would be called twice, the second time with a `URLError.cancelled` error. Only if the second call returned `ConnectionErrorAction.shutdown` would the `EventSource` client actually shutdown. This has been corrected to only call the `connectionErrorHandler` once, and will shutdown the client if `ConnectionErrorAction.shutdown` is returned. Thanks to @tomasf for the PR ([#13](https://github.com/launchdarkly/swift-eventsource/pull/13)). - A race condition that could cause the `EventSource` client to restart after shutting down has been fixed. ## [1.2.0] - 2020-10-21 ### Added - Added `headerTransform` closure to `LDConfig` to allow dynamic http header configuration. ## [1.1.0] - 2020-07-20 ### Added - Support `arm64e` on `appletvos`, `iphoneos`, and `macosx` SDKs by extending valid architectures. - Support for building LDSwiftEventSource on Linux. Currently this library will not generate log messages on Linux, and may not behave correctly on Linux due to Foundation being [incomplete](https://github.com/apple/swift-corelibs-foundation/blob/main/Docs/Status.md). ## [1.0.0] - 2020-07-16 This is the first public release of the LDSwiftEventSource library. The following notes are what changed since the previous pre-release version. ### Changed - Renamed `EventHandler.onMessage` parameter `event` to `eventType`. - The `EventSource` class no longer extends `NSObject` or `URLSessionDataDelegate` to not expose `urlSession` functions. ## [0.5.0] - 2020-07-14 ### Changed - Default `LDSwiftEventSource` product defined for the SwiftPM package is now explicitly a dynamic product. An explicitly static product is now available as `LDSwiftEventSourceStatic`. ## [0.4.0] - 2020-07-13 ### Changed - Converted build system to use a single target to produce a universal framework, rather than separate targets for each platform that share a product name. This is to prevent issues with `xcodebuild` resolving the build scheme to an incorrect platform when building dependent packages with 'Find Implicit Dependencies' enabled. This is due to a bug in `xcodebuild`, for more information see [http://www.openradar.me/20490378](http://www.openradar.me/20490378) and [http://www.openradar.me/22008701](http://www.openradar.me/22008701). ## [0.3.0] - 2020-06-02 ### Added - Added `stop()` method to shutdown the EventSource connection. ### Changed - Logging `subsystem` renamed from `com.launchdarkly.swift-event-source` to `com.launchdarkly.swift-eventsource` ## [0.2.0] - 2020-05-21 ### Added - Public constructors for `UnsuccessfulResponseError` and `MessageEvent` to allow consumers of the library to use them for unit tests. ## [0.1.0] - 2020-05-09 ### Added - Initial implementation for internal alpha testing. ================================================ FILE: CODEOWNERS ================================================ # Repository Maintainers * @launchdarkly/team-sdk-swift ================================================ FILE: CONTRIBUTING.md ================================================ Contributing to the LDSwiftEventSource library ================================================ Submitting bug reports and feature requests ------------------ The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/swift-eventsource/issues) for the EventSource repository. Bug reports and feature requests specific to this library should be filed in this issue tracker. Submitting pull requests ------------------ We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. Build instructions ------------------ ### Prerequisites This library is built with [XCode](https://developer.apple.com/xcode/) or [SwiftPM](https://swift.org/package-manager/). The [CI build](https://github.com/launchdarkly/swift-eventsource/actions/workflows/ci.yml) builds and tests various configurations of the library on various systems, platforms, and devices. For details, see [the GitHub action CI configuration][ci-config]. ### Building And Testing This library can be built directly with the Swift package manager, or through XCode. To build and run tests using SwiftPM simply: ```bash swift test ``` Or in XCode, simply select the desired target and select `Product -> Test`. For building on the command line with `xcodebuild`, see the [continuous integration build configuration][ci-config] for examples on building and running tests. ### Running contract tests To run the standardized contract tests that are run against all LaunchDarkly SSE client implementations: ``` make contract-tests ``` ### Generating API documentation Docs are built with [jazzy](https://github.com/realm/jazzy), which is configured [here](https://github.com/launchdarkly/swift-eventsource/blob/main/.jazzy.yaml). To build them, simply run `jazzy`. Pull requests should keep our documentation coverage at 100%. [ci-config]: https://github.com/launchdarkly/swift-eventsource/blob/main/.github/workflows/ci.yml ================================================ FILE: ContractTestService/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ FILE: ContractTestService/Package.resolved ================================================ { "object": { "pins": [ { "package": "Socket", "repositoryURL": "https://github.com/Kitura/BlueSocket.git", "state": { "branch": null, "revision": "c9894fd117457f1d006575fbfb2fdfd6f79eac03", "version": "1.0.200" } }, { "package": "SSLService", "repositoryURL": "https://github.com/Kitura/BlueSSLService.git", "state": { "branch": null, "revision": "ae5889d2a8b068d2d3ab91ec73aa8f557c03cd8a", "version": "1.0.200" } }, { "package": "Kitura", "repositoryURL": "https://github.com/Kitura/Kitura", "state": { "branch": null, "revision": "daf6479097469a0c1fe64755db6e23f788a3ec29", "version": "2.9.200" } }, { "package": "Kitura-net", "repositoryURL": "https://github.com/Kitura/Kitura-net.git", "state": { "branch": null, "revision": "e017a57772eb477c294d72b25ab9ae38d25539f5", "version": "2.4.200" } }, { "package": "Kitura-TemplateEngine", "repositoryURL": "https://github.com/Kitura/Kitura-TemplateEngine.git", "state": { "branch": null, "revision": "1670b09bfb63b66f7dffff627a06b50492485407", "version": "2.0.200" } }, { "package": "KituraContracts", "repositoryURL": "https://github.com/Kitura/KituraContracts.git", "state": { "branch": null, "revision": "8a4778c3aa7833e9e1af884e8819d436c237cd70", "version": "1.2.201" } }, { "package": "LoggerAPI", "repositoryURL": "https://github.com/Kitura/LoggerAPI.git", "state": { "branch": null, "revision": "e82d34eab3f0b05391082b11ea07d3b70d2f65bb", "version": "1.9.200" } }, { "package": "swift-log", "repositoryURL": "https://github.com/apple/swift-log.git", "state": { "branch": null, "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", "version": "1.4.2" } }, { "package": "TypeDecoder", "repositoryURL": "https://github.com/Kitura/TypeDecoder.git", "state": { "branch": null, "revision": "28ec01815c0aea9236f92982ca8d351e7112a4a0", "version": "1.3.201" } } ] }, "version": 1 } ================================================ FILE: ContractTestService/Package.swift ================================================ // swift-tools-version:5.0 import PackageDescription let package = Package( name: "ContractTestService", platforms: [ .iOS(.v11), .macOS(.v10_13), .watchOS(.v4), .tvOS(.v11), ], products: [ .executable( name: "contract-test-service", targets: ["ContractTestService"] ) ], dependencies: [ // Local dependency to LDSwiftEventSource .package(path: ".."), .package(url: "https://github.com/Kitura/Kitura", from: "2.9.200") ], targets: [ .target( name: "ContractTestService", dependencies: [ "LDSwiftEventSource", "Kitura" ] ) ] ) ================================================ FILE: ContractTestService/README.md ================================================ # SSE client contract test service This directory contains an implementation of the cross-platform SSE testing protocol defined by https://github.com/launchdarkly/sse-contract-tests. See that project's `README` for details of this protocol, and the kinds of SSE client capabilities that are relevant to the contract tests. This code should not need to be updated unless the SSE client has added or removed such capabilities. To run these tests locally, run `make contract-tests` from the project root directory. This downloads the correct version of the test harness tool automatically. ================================================ FILE: ContractTestService/Sources/ContractTestService/main.swift ================================================ import Dispatch import Foundation import Kitura import LDSwiftEventSource struct StatusResp: Encodable { let name = "swift-eventsource" let capabilities = ["server-directed-shutdown-request", "comments", "headers", "last-event-id", "post", "read-timeout", "report"] } struct CreateStreamReq: Decodable { let streamUrl: URL let callbackUrl: URL let initialDelayMs: Int? let readTimeoutMs: Int? let lastEventId: String? let headers: [String: String]? let method: String? let body: String? func createEventSourceConfig() -> EventSource.Config { var esConfig = EventSource.Config(handler: CallbackHandler(baseUrl: callbackUrl), url: streamUrl) if let initialDelayMs = initialDelayMs { esConfig.reconnectTime = Double(initialDelayMs) / 1000.0 } if let readTimeoutMs = readTimeoutMs { esConfig.idleTimeout = Double(readTimeoutMs) / 1000.0 } if let lastEventId = lastEventId { esConfig.lastEventId = lastEventId } if let headers = headers { esConfig.headers = headers } if let method = method { esConfig.method = method } if let body = body { esConfig.body = Data(body.utf8) } return esConfig } } class CallbackHandler: EventHandler { struct EventPayloadEvent: Encodable { let type: String let data: String let id: String? } struct EventPayload: Encodable { let kind = "event" let event: EventPayloadEvent } struct CommentPayload: Encodable { let kind = "comment" let comment: String } struct ErrorPayload: Encodable { let kind = "error" } let baseUrl: URL var count = 0 init(baseUrl: URL) { self.baseUrl = baseUrl } func onOpened() { } func onClosed() { } func sendUpdate(_ update: T) { count += 1 var request = URLRequest(url: baseUrl.appendingPathComponent(String(count), isDirectory: false)) request.httpMethod = "POST" let data = try! JSONEncoder().encode(update) URLSession.shared.uploadTask(with: request, from: data) { _, _, _ in }.resume() } func onMessage(eventType type: String, messageEvent msg: MessageEvent) { sendUpdate(EventPayload(event: EventPayloadEvent(type: type, data: msg.data, id: msg.lastEventId))) } func onComment(comment: String) { sendUpdate(CommentPayload(comment: comment)) } func onError(error: Error) { sendUpdate(ErrorPayload()) } } let stateQueue = DispatchQueue(label: "StateQueue") var nextId: Int = 0 var state: [String: EventSource] = [:] let router = Router() router.get("/") { _, resp, next in resp.send(StatusResp()) next() } router.delete("/") { _, resp, next in resp.send(["message": "Shutting down contract test service"]) next() Kitura.stop() } router.post("/") { req, resp, next in guard let createStreamReq = try? req.read(as: CreateStreamReq.self) else { resp.status(.badRequest).send(["message": "Body of POST to '/' invalid"]) return next() } let es = EventSource(config: createStreamReq.createEventSourceConfig()) let location: String = stateQueue.sync { state[String(nextId)] = es nextId += 1 return "/control/\(nextId - 1)" } es.start() resp.headers["Location"] = location resp.send(["message": "Created test service entity at \(location)"]) next() } router.delete("/control/:id") { req, resp, next in stateQueue.sync { if let es = state.removeValue(forKey: req.parameters["id"]!) { es.stop() resp.send(["message": "Shut down test service entity at \(req.matchedPath)"]) } else { resp.status(.notFound).send(["message": "Test service entity not found at \(req.matchedPath)"]) } } next() } Kitura.addHTTPServer(onPort: 8000, onAddress: "localhost", with: router) Kitura.run() ================================================ FILE: LDSwiftEventSource.podspec ================================================ Pod::Spec.new do |s| s.name = "LDSwiftEventSource" s.version = "3.3.0" # x-release-please-version s.summary = "Swift EventSource library" s.homepage = "https://github.com/launchdarkly/swift-eventsource" s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE.txt" } s.author = { "LaunchDarkly" => "sdks@launchdarkly.com" } s.ios.deployment_target = "11.0" s.watchos.deployment_target = "4.0" s.tvos.deployment_target = "11.0" s.osx.deployment_target = "10.13" s.source = { :git => s.homepage + '.git', :tag => s.version} s.source_files = "Source/**/*.swift" s.swift_versions = ['5.0', '5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '5.7'] end ================================================ FILE: LDSwiftEventSource.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ B426585E272849AF007B711A /* MockHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B426585D272849AF007B711A /* MockHandler.swift */; }; B495D4A9248652DF00AE9233 /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = B495D4A7248652DF00AE9233 /* Types.swift */; }; B49B5E4B24667F62008BF867 /* UTF8LineParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49B5E4524667F43008BF867 /* UTF8LineParser.swift */; }; B49B5E4C24667F62008BF867 /* EventParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49B5E4624667F43008BF867 /* EventParser.swift */; }; B49B5E4D24667F62008BF867 /* LDSwiftEventSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49B5E4724667F43008BF867 /* LDSwiftEventSource.swift */; }; B49B5E5824668031008BF867 /* EventParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49B5E4024667F43008BF867 /* EventParserTests.swift */; }; B49B5E5A24668031008BF867 /* UTF8LineParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49B5E4224667F43008BF867 /* UTF8LineParserTests.swift */; }; B49B5E5B24668031008BF867 /* LDSwiftEventSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49B5E4324667F43008BF867 /* LDSwiftEventSourceTests.swift */; }; B49B5E67246684B9008BF867 /* LDSwiftEventSource.h in Headers */ = {isa = PBXBuildFile; fileRef = B49B5E65246684B9008BF867 /* LDSwiftEventSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; B49B5E72246C4796008BF867 /* LDSwiftEventSource.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B49B5DFC24667D41008BF867 /* LDSwiftEventSource.framework */; }; B4BCAE6E272753FA000EBD43 /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4BCAE6D272753FA000EBD43 /* TestUtil.swift */; }; B4C29CC826FF743D008B6DE2 /* Logs.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C29CC726FF743C008B6DE2 /* Logs.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ B49B5E0624667D42008BF867 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = B49B5DB824667C44008BF867 /* Project object */; proxyType = 1; remoteGlobalIDString = B49B5DFB24667D41008BF867; remoteInfo = "LDSwiftEventSource macOS"; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ B426585D272849AF007B711A /* MockHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHandler.swift; sourceTree = ""; }; B495D4A7248652DF00AE9233 /* Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Types.swift; sourceTree = ""; }; B49B5DE324667D06008BF867 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B49B5DFC24667D41008BF867 /* LDSwiftEventSource.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LDSwiftEventSource.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B49B5E0424667D42008BF867 /* LDSwiftEventSource Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "LDSwiftEventSource Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; B49B5E4024667F43008BF867 /* EventParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventParserTests.swift; sourceTree = ""; }; B49B5E4224667F43008BF867 /* UTF8LineParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTF8LineParserTests.swift; sourceTree = ""; }; B49B5E4324667F43008BF867 /* LDSwiftEventSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDSwiftEventSourceTests.swift; sourceTree = ""; }; B49B5E4524667F43008BF867 /* UTF8LineParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTF8LineParser.swift; sourceTree = ""; }; B49B5E4624667F43008BF867 /* EventParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventParser.swift; sourceTree = ""; }; B49B5E4724667F43008BF867 /* LDSwiftEventSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDSwiftEventSource.swift; sourceTree = ""; }; B49B5E65246684B9008BF867 /* LDSwiftEventSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDSwiftEventSource.h; sourceTree = ""; }; B49B5E6B2466875F008BF867 /* LDSwiftEventSource.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = LDSwiftEventSource.podspec; sourceTree = ""; }; B49B5E6C2466875F008BF867 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; B49B5E6D2466875F008BF867 /* CONTRIBUTING.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CONTRIBUTING.md; sourceTree = ""; }; B49B5E6E2466875F008BF867 /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; B49B5E6F2466875F008BF867 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; B49B5E702466875F008BF867 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; B4BCAE6D272753FA000EBD43 /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = ""; }; B4C29CC726FF743C008B6DE2 /* Logs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logs.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ B49B5DF924667D41008BF867 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; B49B5E0124667D42008BF867 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( B49B5E72246C4796008BF867 /* LDSwiftEventSource.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ B49B5DB724667C44008BF867 = { isa = PBXGroup; children = ( B49B5E6A2466873F008BF867 /* Misc */, B49B5E4424667F43008BF867 /* Source */, B49B5E3F24667F43008BF867 /* Tests */, B49B5DC224667C44008BF867 /* Products */, ); sourceTree = ""; }; B49B5DC224667C44008BF867 /* Products */ = { isa = PBXGroup; children = ( B49B5DFC24667D41008BF867 /* LDSwiftEventSource.framework */, B49B5E0424667D42008BF867 /* LDSwiftEventSource Tests.xctest */, ); name = Products; sourceTree = ""; }; B49B5E3F24667F43008BF867 /* Tests */ = { isa = PBXGroup; children = ( B49B5E4024667F43008BF867 /* EventParserTests.swift */, B49B5E4224667F43008BF867 /* UTF8LineParserTests.swift */, B49B5E4324667F43008BF867 /* LDSwiftEventSourceTests.swift */, B4BCAE6D272753FA000EBD43 /* TestUtil.swift */, B426585D272849AF007B711A /* MockHandler.swift */, ); path = Tests; sourceTree = ""; }; B49B5E4424667F43008BF867 /* Source */ = { isa = PBXGroup; children = ( B49B5DE324667D06008BF867 /* Info.plist */, B49B5E4524667F43008BF867 /* UTF8LineParser.swift */, B49B5E4624667F43008BF867 /* EventParser.swift */, B49B5E4724667F43008BF867 /* LDSwiftEventSource.swift */, B49B5E65246684B9008BF867 /* LDSwiftEventSource.h */, B495D4A7248652DF00AE9233 /* Types.swift */, B4C29CC726FF743C008B6DE2 /* Logs.swift */, ); path = Source; sourceTree = ""; }; B49B5E6A2466873F008BF867 /* Misc */ = { isa = PBXGroup; children = ( B49B5E6C2466875F008BF867 /* CHANGELOG.md */, B49B5E6D2466875F008BF867 /* CONTRIBUTING.md */, B49B5E6B2466875F008BF867 /* LDSwiftEventSource.podspec */, B49B5E6E2466875F008BF867 /* LICENSE.txt */, B49B5E702466875F008BF867 /* Package.swift */, B49B5E6F2466875F008BF867 /* README.md */, ); name = Misc; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ B49B5DF724667D41008BF867 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( B49B5E67246684B9008BF867 /* LDSwiftEventSource.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ B49B5DFB24667D41008BF867 /* LDSwiftEventSource */ = { isa = PBXNativeTarget; buildConfigurationList = B49B5E0D24667D42008BF867 /* Build configuration list for PBXNativeTarget "LDSwiftEventSource" */; buildPhases = ( B46C1C6B24CF348B00283630 /* Linter Script */, B49B5DF724667D41008BF867 /* Headers */, B49B5DF824667D41008BF867 /* Sources */, B49B5DF924667D41008BF867 /* Frameworks */, B49B5DFA24667D41008BF867 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = LDSwiftEventSource; productName = "LDSwiftEventSource macOS"; productReference = B49B5DFC24667D41008BF867 /* LDSwiftEventSource.framework */; productType = "com.apple.product-type.framework"; }; B49B5E0324667D42008BF867 /* LDSwiftEventSource Tests */ = { isa = PBXNativeTarget; buildConfigurationList = B49B5E1024667D42008BF867 /* Build configuration list for PBXNativeTarget "LDSwiftEventSource Tests" */; buildPhases = ( B49B5E0024667D42008BF867 /* Sources */, B49B5E0124667D42008BF867 /* Frameworks */, B49B5E0224667D42008BF867 /* Resources */, ); buildRules = ( ); dependencies = ( B49B5E0724667D42008BF867 /* PBXTargetDependency */, ); name = "LDSwiftEventSource Tests"; productName = "LDSwiftEventSource macOSTests"; productReference = B49B5E0424667D42008BF867 /* LDSwiftEventSource Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ B49B5DB824667C44008BF867 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1140; LastUpgradeCheck = 1140; ORGANIZATIONNAME = LaunchDarkly; TargetAttributes = { B49B5DFB24667D41008BF867 = { CreatedOnToolsVersion = 11.4; }; B49B5E0324667D42008BF867 = { CreatedOnToolsVersion = 11.4; }; }; }; buildConfigurationList = B49B5DBB24667C44008BF867 /* Build configuration list for PBXProject "LDSwiftEventSource" */; compatibilityVersion = "Xcode 10.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = B49B5DB724667C44008BF867; productRefGroup = B49B5DC224667C44008BF867 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( B49B5DFB24667D41008BF867 /* LDSwiftEventSource */, B49B5E0324667D42008BF867 /* LDSwiftEventSource Tests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ B49B5DFA24667D41008BF867 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; B49B5E0224667D42008BF867 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ B46C1C6B24CF348B00283630 /* Linter Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Linter Script"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "# Adds support for Apple Silicon brew directory\nexport PATH=\"$PATH:/opt/homebrew/bin\"\n\nif which mint >/dev/null; then\n /usr/bin/xcrun --sdk macosx mint run realm/SwiftLint\nelse\n echo \"warning: mint not installed, available from https://github.com/yonaskolb/Mint\"\nfi\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ B49B5DF824667D41008BF867 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( B49B5E4B24667F62008BF867 /* UTF8LineParser.swift in Sources */, B4C29CC826FF743D008B6DE2 /* Logs.swift in Sources */, B495D4A9248652DF00AE9233 /* Types.swift in Sources */, B49B5E4C24667F62008BF867 /* EventParser.swift in Sources */, B49B5E4D24667F62008BF867 /* LDSwiftEventSource.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; B49B5E0024667D42008BF867 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( B49B5E5824668031008BF867 /* EventParserTests.swift in Sources */, B49B5E5A24668031008BF867 /* UTF8LineParserTests.swift in Sources */, B426585E272849AF007B711A /* MockHandler.swift in Sources */, B49B5E5B24668031008BF867 /* LDSwiftEventSourceTests.swift in Sources */, B4BCAE6E272753FA000EBD43 /* TestUtil.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ B49B5E0724667D42008BF867 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = B49B5DFB24667D41008BF867 /* LDSwiftEventSource */; targetProxy = B49B5E0624667D42008BF867 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ B49B5DD324667C44008BF867 /* 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 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[sdk=iphoneos*]" = "iPhone Developer"; "COMBINE_HIDPI_IMAGES[sdk=macosx]" = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_VERSION = C; 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; INFOPLIST_FILE = Source/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.LDSwiftEventSource; PRODUCT_NAME = LDSwiftEventSource; SDKROOT = macosx; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 11.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Debug; }; B49B5DD424667C44008BF867 /* 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 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[sdk=iphoneos*]" = "iPhone Developer"; "COMBINE_HIDPI_IMAGES[sdk=macosx]" = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_VERSION = C; 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; INFOPLIST_FILE = Source/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.LDSwiftEventSource; PRODUCT_NAME = LDSwiftEventSource; SDKROOT = macosx; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TVOS_DEPLOYMENT_TARGET = 11.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; WATCHOS_DEPLOYMENT_TARGET = 4.0; }; name = Release; }; B49B5E0E24667D42008BF867 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 3.0.0; DYLIB_CURRENT_VERSION = 3.3.0; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = YES; "ENABLE_BITCODE[sdk=macosx*]" = NO; INFOPLIST_FILE = "$(inherited)"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 3.3.0; SKIP_INSTALL = YES; "TARGETED_DEVICE_FAMILY[sdk=appletvos*]" = 3; "TARGETED_DEVICE_FAMILY[sdk=appletvsimulator*]" = 3; "TARGETED_DEVICE_FAMILY[sdk=iphoneos*]" = "1,2"; "TARGETED_DEVICE_FAMILY[sdk=iphonesimulator*]" = "1,2"; "TARGETED_DEVICE_FAMILY[sdk=watchos*]" = 4; "TARGETED_DEVICE_FAMILY[sdk=watchsimulator*]" = 4; }; name = Debug; }; B49B5E0F24667D42008BF867 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 3.0.0; DYLIB_CURRENT_VERSION = 3.3.0; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_BITCODE = YES; "ENABLE_BITCODE[sdk=macosx*]" = NO; INFOPLIST_FILE = "$(inherited)"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 3.3.0; SKIP_INSTALL = YES; "TARGETED_DEVICE_FAMILY[sdk=appletvos*]" = 3; "TARGETED_DEVICE_FAMILY[sdk=appletvsimulator*]" = 3; "TARGETED_DEVICE_FAMILY[sdk=iphoneos*]" = "1,2"; "TARGETED_DEVICE_FAMILY[sdk=iphonesimulator*]" = "1,2"; "TARGETED_DEVICE_FAMILY[sdk=watchos*]" = 4; "TARGETED_DEVICE_FAMILY[sdk=watchsimulator*]" = 4; }; name = Release; }; B49B5E1124667D42008BF867 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = "$(inherited)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.LDSwiftEventSourceTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator"; }; name = Debug; }; B49B5E1224667D42008BF867 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = "$(inherited)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( "$(inherited)", "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.LDSwiftEventSourceTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ B49B5DBB24667C44008BF867 /* Build configuration list for PBXProject "LDSwiftEventSource" */ = { isa = XCConfigurationList; buildConfigurations = ( B49B5DD324667C44008BF867 /* Debug */, B49B5DD424667C44008BF867 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; B49B5E0D24667D42008BF867 /* Build configuration list for PBXNativeTarget "LDSwiftEventSource" */ = { isa = XCConfigurationList; buildConfigurations = ( B49B5E0E24667D42008BF867 /* Debug */, B49B5E0F24667D42008BF867 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; B49B5E1024667D42008BF867 /* Build configuration list for PBXNativeTarget "LDSwiftEventSource Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( B49B5E1124667D42008BF867 /* Debug */, B49B5E1224667D42008BF867 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = B49B5DB824667C44008BF867 /* Project object */; } ================================================ FILE: LDSwiftEventSource.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: LDSwiftEventSource.xcodeproj/xcshareddata/xcschemes/LDSwiftEventSource.xcscheme ================================================ ================================================ FILE: LICENSE.txt ================================================ Copyright 2020 Catamorphic, Co. 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. ================================================ FILE: Makefile ================================================ build: swift build clean: swift clean test: swift test TEMP_TEST_OUTPUT=/tmp/sse-contract-test-service.log build-contract-tests: cd ContractTestService && swift build start-contract-test-service: ./ContractTestService/.build/debug/contract-test-service start-contract-test-service-bg: echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)" make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 & run-contract-tests: curl -s https://raw.githubusercontent.com/launchdarkly/sse-contract-tests/main/downloader/run.sh \ | VERSION=v2 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end -skip 'basic parsing/large message in one chunk' -skip 'basic parsing/large message in two chunks'" sh contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests .PHONY: build clean test build-contract-tests start-contract-test-service run-contract-tests contract-tests ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.0 import PackageDescription let package = Package( name: "LDSwiftEventSource", platforms: [ .iOS(.v11), .macOS(.v10_13), .watchOS(.v4), .tvOS(.v11) ], products: [ .library(name: "LDSwiftEventSource", targets: ["LDSwiftEventSource"]), ], dependencies: [], targets: [ .target( name: "LDSwiftEventSource", path: "Source"), .testTarget( name: "LDSwiftEventSourceTests", dependencies: ["LDSwiftEventSource"], path: "Tests"), ], swiftLanguageVersions: [.v5]) ================================================ FILE: README.md ================================================ # LDSwiftEventSource [![Run CI](https://github.com/launchdarkly/swift-eventsource/actions/workflows/ci.yml/badge.svg)](https://github.com/launchdarkly/swift-eventsource/actions/workflows/ci.yml) [![CocoaPods](https://img.shields.io/cocoapods/v/LDSwiftEventSource.svg)](https://cocoapods.org/pods/LDSwiftEventSource) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-4BC51D.svg?style=flat)](https://swift.org/package-manager/) [![Platform](https://img.shields.io/cocoapods/p/LDSwiftEventSource.svg?style=flat)](https://cocoapods.org/pods/LDSwiftEventSource) LDSwiftEventSource is a cross platform implementation of the [EventSource specification](https://html.spec.whatwg.org/multipage/server-sent-events.html) written in Swift. It was developed for use in the [LaunchDarkly iOS SDK](https://github.com/launchdarkly/ios-client-sdk). Generated API docs are available on [GitHub Pages](https://launchdarkly.github.io/swift-eventsource/). ## Requirements - iOS 11.0+ / watchOS 4.0+ / tvOS 11.0+ / macOS 10.13+ - Swift 5.1+ ## Installation ### CocoaPods To use the [CocoaPods](https://cocoapods.org) dependency manager to integrate LDSwiftEventSource into your Xcode project, specify it in your `Podfile`: ```ruby pod 'LDSwiftEventSource', '~> 3.3' ``` ### Carthage To use the [Carthage](https://github.com/Carthage/Carthage) dependency manager to integrate LDSwiftEventSource into your Xcode project, specify it in your `Cartfile`: ```ogdl github "LaunchDarkly/swift-eventsource" ~> 3.3 ``` ### Swift Package Manager The [Swift Package Manager](https://swift.org/package-manager/) is a dependency manager integrated into the `swift` compiler and Xcode. Note that the LDSwiftEventSource Swift package provides both a `LDSwiftEventSource` product, which is explicitly dynamic, and a `LDSwiftEventSourceStatic` product which is explicitly static. To integrate LDSwiftEventSource into an Xcode project, go to the project editor, and select `Swift Packages`. From here hit the `+` button and follow the prompts using `https://github.com/LaunchDarkly/swift-eventsource.git` as the URL. To include LDSwiftEventSource in a Swift package, simply add it to the dependencies section of your `Package.swift` file. And add the desired product as a dependency for your targets. ```swift dependencies: [ .package(url: "https://github.com/LaunchDarkly/swift-eventsource.git", .upToNextMajor(from: "3.3.0")) ] ``` ## Contributing We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](https://github.com/LaunchDarkly/swift-eventsource/blob/main/CONTRIBUTING.md) for instructions on how to contribute to this SDK. ## About LaunchDarkly * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. * Explore LaunchDarkly * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates ================================================ FILE: SECURITY.md ================================================ # Reporting and Fixing Security Issues Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty. Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors. ================================================ FILE: Source/.swiftlint.yml ================================================ disabled_rules: opt_in_rules: - force_unwrapping - implicitly_unwrapped_optional ================================================ FILE: Source/EventParser.swift ================================================ import Foundation class EventParser { private struct Constants { static let dataLabel: Substring = "data" static let idLabel: Substring = "id" static let eventLabel: Substring = "event" static let retryLabel: Substring = "retry" } private let handler: EventHandler private var data: String = "" private var eventType: String = "" private var lastEventIdBuffer: String? private var lastEventId: String private var currentRetry: TimeInterval init(handler: EventHandler, initialEventId: String, initialRetry: TimeInterval) { self.handler = handler self.lastEventId = initialEventId self.currentRetry = initialRetry } func parse(line: String) { let splitByColon = line.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) switch (splitByColon[0], splitByColon[safe: 1]) { case ("", nil): // Empty line dispatchEvent() case let ("", .some(comment)): // Line starting with ':' is a comment handler.onComment(comment: String(comment)) case let (field, data): processField(field: field, value: dropLeadingSpace(str: data ?? "")) } } func getLastEventId() -> String { lastEventId } func reset() -> TimeInterval { data = "" eventType = "" lastEventIdBuffer = nil return currentRetry } private func dropLeadingSpace(str: Substring) -> Substring { if str.first == " " { return str[str.index(after: str.startIndex)...] } return str } private func processField(field: Substring, value: Substring) { switch field { case Constants.dataLabel: data.append(contentsOf: value) data.append(contentsOf: "\n") case Constants.idLabel: // See https://github.com/whatwg/html/issues/689 for reasoning on not setting lastEventId if the value // contains a null code point. if !value.contains("\u{0000}") { lastEventIdBuffer = String(value) } case Constants.eventLabel: eventType = String(value) case Constants.retryLabel: if value.allSatisfy(("0"..."9").contains), let reconnectionTime = Int64(value) { currentRetry = Double(reconnectionTime) * 0.001 } default: break } } private func dispatchEvent() { lastEventId = lastEventIdBuffer ?? lastEventId lastEventIdBuffer = nil guard !data.isEmpty else { eventType = "" return } // remove the last LF _ = data.popLast() let messageEvent = MessageEvent(data: data, lastEventId: lastEventId) handler.onMessage(eventType: eventType.isEmpty ? "message" : eventType, messageEvent: messageEvent) data = "" eventType = "" } } private extension Array { /// Returns the element at the specified index if it is within bounds, otherwise nil. subscript (safe index: Index) -> Element? { index >= startIndex && index < endIndex ? self[index] : nil } } ================================================ FILE: Source/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion $(CURRENT_PROJECT_VERSION) ================================================ FILE: Source/LDSwiftEventSource.h ================================================ @import Foundation; FOUNDATION_EXPORT double LDSwiftEventSourceVersionNumber; FOUNDATION_EXPORT const unsigned char LDSwiftEventSourceVersionString[]; ================================================ FILE: Source/LDSwiftEventSource.swift ================================================ import Foundation #if os(Linux) || os(Windows) import FoundationNetworking #endif #if canImport(os) // os_log is not supported on some platforms, but we want to use it for most of our customer's // use cases that use Apple OSs import os.log #endif /** Provides an EventSource client for consuming Server-Sent Events. See the [Server-Sent Events spec](https://html.spec.whatwg.org/multipage/server-sent-events.html) for more details. */ public class EventSource { private let esDelegate: EventSourceDelegate /** Initialize the `EventSource` client with the given configuration. - Parameter config: The configuration for initializing the `EventSource` client. */ public init(config: Config) { esDelegate = EventSourceDelegate(config: config) } /** Start the `EventSource` client. This will initiate a streaming connection to the configured URL. The application will be informed of received events and state changes using the configured `EventHandler`. */ public func start() { esDelegate.start() } /// Shuts down the `EventSource` client. It is not valid to restart the client after calling this function. public func stop() { esDelegate.stop() } /// Get the most recently received event ID, or the value of `EventSource.Config.lastEventId` if no event IDs have /// been received. public func getLastEventId() -> String? { esDelegate.getLastEventId() } /// Struct for configuring the EventSource. public struct Config { /// The `EventHandler` called in response to activity on the stream. public let handler: EventHandler /// The `URL` of the request used when connecting to the EventSource API. public let url: URL /// The HTTP method to use for the API request. public var method: String = "GET" /// Optional HTTP body to be included in the API request. public var body: Data? /// Additional HTTP headers to be set on the request public var headers: [String: String] = [:] /// Transform function to allow dynamically configuring the headers on each API request. public var headerTransform: HeaderTransform = { $0 } /// An initial value for the last-event-id header to be sent on the initial request public var lastEventId: String = "" #if canImport(os) /// Configure the logger that will be used. public var logger: OSLog = OSLog(subsystem: "com.launchdarkly.swift-eventsource", category: "LDEventSource") #endif /// The minimum amount of time to wait before reconnecting after a failure public var reconnectTime: TimeInterval = 1.0 /// The maximum amount of time to wait before reconnecting after a failure public var maxReconnectTime: TimeInterval = 30.0 /// The minimum amount of time for an `EventSource` connection to remain open before allowing the connection /// backoff to reset. public var backoffResetThreshold: TimeInterval = 60.0 /// The maximum amount of time between receiving any data before considering the connection to have timed out. public var idleTimeout: TimeInterval = 300.0 private var _urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default /** The `URLSessionConfiguration` used to create the `URLSession`. - Important: Note that this copies the given `URLSessionConfiguration` when set, and returns copies (updated with any overrides specified by other configuration options) when the value is retrieved. This prevents updating the `URLSessionConfiguration` after initializing `EventSource` with the `Config`, and prevents the `EventSource` from updating any properties of the given `URLSessionConfiguration`. - Since: 1.3.0 */ public var urlSessionConfiguration: URLSessionConfiguration { get { // swiftlint:disable:next force_cast let sessionConfig = _urlSessionConfiguration.copy() as! URLSessionConfiguration sessionConfig.httpAdditionalHeaders = ["Accept": "text/event-stream", "Cache-Control": "no-cache"] sessionConfig.timeoutIntervalForRequest = idleTimeout #if !os(Linux) && !os(Windows) if #available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *) { sessionConfig.tlsMinimumSupportedProtocolVersion = .TLSv12 } else { sessionConfig.tlsMinimumSupportedProtocol = .tlsProtocol12 } #endif return sessionConfig } set { // swiftlint:disable:next force_cast _urlSessionConfiguration = newValue.copy() as! URLSessionConfiguration } } /** An error handler that is called when an error occurs and can shut down the client in response. The default error handler will always attempt to reconnect on an error, unless `EventSource.stop()` is called or the error code is 204. */ public var connectionErrorHandler: ConnectionErrorHandler = { error in guard let unsuccessfulResponseError = error as? UnsuccessfulResponseError else { return .proceed } let responseCode: Int = unsuccessfulResponseError.responseCode if 204 == responseCode { return .shutdown } return .proceed } /// Create a new configuration with an `EventHandler` and a `URL` public init(handler: EventHandler, url: URL) { self.handler = handler self.url = url } } } class ReconnectionTimer { private let maxDelay: TimeInterval private let resetInterval: TimeInterval var backoffCount: Int = 0 var connectedTime: Date? init(maxDelay: TimeInterval, resetInterval: TimeInterval) { self.maxDelay = maxDelay self.resetInterval = resetInterval } func reconnectDelay(baseDelay: TimeInterval) -> TimeInterval { backoffCount += 1 if let connectedTime = connectedTime, Date().timeIntervalSince(connectedTime) >= resetInterval { backoffCount = 0 } self.connectedTime = nil let maxSleep = min(maxDelay, baseDelay * pow(2.0, Double(backoffCount))) return maxSleep / 2 + Double.random(in: 0...(maxSleep / 2)) } } // MARK: EventSourceDelegate class EventSourceDelegate: NSObject, URLSessionDataDelegate { private let delegateQueue: DispatchQueue = DispatchQueue(label: "ESDelegateQueue") public var logger: InternalLogging private let config: EventSource.Config private var readyState: ReadyState = .raw { didSet { logger.log(.debug, "State: %@ -> %@", oldValue.rawValue, readyState.rawValue) } } private let utf8LineParser: UTF8LineParser = UTF8LineParser() private let eventParser: EventParser private let reconnectionTimer: ReconnectionTimer private var urlSession: URLSession? private var sessionTask: URLSessionDataTask? init(config: EventSource.Config) { self.config = config #if canImport(os) self.logger = OSLogAdapter(osLog: config.logger) #else self.logger = NoOpLogging() #endif self.eventParser = EventParser(handler: config.handler, initialEventId: config.lastEventId, initialRetry: config.reconnectTime) self.reconnectionTimer = ReconnectionTimer(maxDelay: config.maxReconnectTime, resetInterval: config.backoffResetThreshold) } func start() { delegateQueue.async { [weak self] in guard let self = self else { return } guard self.readyState == .raw else { self.logger.log(.info, "start() called on already-started EventSource object. Returning") return } self.readyState = .connecting self.urlSession = self.createSession() self.connect() } } func stop() { delegateQueue.async { let previousState = self.readyState self.readyState = .shutdown self.sessionTask?.cancel() if previousState == .open { self.config.handler.onClosed() } self.urlSession?.invalidateAndCancel() self.urlSession = nil } } func getLastEventId() -> String { eventParser.getLastEventId() } func createSession() -> URLSession { let opQueue = OperationQueue() opQueue.underlyingQueue = self.delegateQueue return URLSession(configuration: config.urlSessionConfiguration, delegate: self, delegateQueue: opQueue) } func createRequest() -> URLRequest { var urlRequest = URLRequest(url: self.config.url, cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: self.config.idleTimeout) urlRequest.httpMethod = self.config.method urlRequest.httpBody = self.config.body if !eventParser.getLastEventId().isEmpty { urlRequest.setValue(eventParser.getLastEventId(), forHTTPHeaderField: "Last-Event-Id") } urlRequest.allHTTPHeaderFields = self.config.headerTransform( urlRequest.allHTTPHeaderFields?.merging(self.config.headers) { $1 } ?? self.config.headers ) return urlRequest } private func connect() { logger.log(.info, "Starting EventSource client") let task = urlSession?.dataTask(with: createRequest()) task?.resume() sessionTask = task } func dispatchError(error: Error) -> ConnectionErrorAction { let action: ConnectionErrorAction = config.connectionErrorHandler(error) if action != .shutdown { config.handler.onError(error: error) } return action } // MARK: URLSession Delegates // Tells the delegate that the task finished transferring data. public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { utf8LineParser.closeAndReset() let currentRetry = eventParser.reset() guard readyState != .shutdown else { return } if let error = error { if (error as NSError).code != NSURLErrorCancelled { logger.log(.info, "Connection error: %@", error.localizedDescription) if dispatchError(error: error) == .shutdown { logger.log(.info, "Connection has been explicitly shut down by error handler") if readyState == .open { config.handler.onClosed() } readyState = .shutdown return } } } else { logger.log(.info, "Connection unexpectedly closed.") } if readyState == .open { config.handler.onClosed() } readyState = .closed let sleep = reconnectionTimer.reconnectDelay(baseDelay: currentRetry) // this formatting shenanigans is to workaround String not implementing CVarArg on Swift<5.4 on Linux logger.log(.info, "Waiting %@ seconds before reconnecting...", String(format: "%.3f", sleep)) delegateQueue.asyncAfter(deadline: .now() + sleep) { [weak self] in self?.connect() } } // Tells the delegate that the data task received the initial reply (headers) from the server. public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { logger.log(.debug, "Initial reply received") guard readyState != .shutdown else { completionHandler(.cancel) return } // swiftlint:disable:next force_cast let httpResponse = response as! HTTPURLResponse let statusCode = httpResponse.statusCode if (200..<300).contains(statusCode) && statusCode != 204 { reconnectionTimer.connectedTime = Date() readyState = .open config.handler.onOpened() completionHandler(.allow) } else { // this formatting shenanigans is to workaround String not implementing CVarArg on Swift<5.4 on Linux logger.log(.info, "Unsuccessful response: %@", String(format: "%d", statusCode)) if dispatchError(error: UnsuccessfulResponseError(responseCode: statusCode)) == .shutdown { logger.log(.info, "Connection has been explicitly shut down by error handler") readyState = .shutdown } completionHandler(.cancel) } } public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { utf8LineParser.append(data).forEach(eventParser.parse) } } ================================================ FILE: Source/Logs.swift ================================================ import Foundation #if canImport(os) import os.log #endif protocol InternalLogging { func log(_ level: Level, _ staticMsg: StaticString) func log(_ level: Level, _ staticMsg: StaticString, _ arg: String) func log(_ level: Level, _ staticMsg: StaticString, _ arg1: String, _ arg2: String) } enum Level { case debug, info, warn, error #if canImport(os) private static let osLogTypes = [ Level.debug: OSLogType.debug, Level.info: OSLogType.info, Level.warn: OSLogType.default, Level.error: OSLogType.error] var osLogType: OSLogType { Level.osLogTypes[self]! } #endif } #if canImport(os) class OSLogAdapter: InternalLogging { private let osLog: OSLog init(osLog: OSLog) { self.osLog = osLog } func log(_ level: Level, _ staticMsg: StaticString) { os_log(staticMsg, log: self.osLog, type: level.osLogType) } func log(_ level: Level, _ staticMsg: StaticString, _ arg: String) { os_log(staticMsg, log: self.osLog, type: level.osLogType, arg) } func log(_ level: Level, _ staticMsg: StaticString, _ arg1: String, _ arg2: String) { os_log(staticMsg, log: self.osLog, type: level.osLogType, arg1, arg2) } } #endif class NoOpLogging: InternalLogging { func log(_ level: Level, _ staticMsg: StaticString) {} func log(_ level: Level, _ staticMsg: StaticString, _ arg: String) {} func log(_ level: Level, _ staticMsg: StaticString, _ arg1: String, _ arg2: String) {} } ================================================ FILE: Source/Types.swift ================================================ import Foundation /** Type for a function that will be notified when the `EventSource` client encounters a connection failure. This is different from `EventHandler.onError(error:)` in that it will not be called for other kinds of errors; also, it has the ability to tell the client to stop reconnecting by returning a `ConnectionErrorAction.shutdown`. */ public typealias ConnectionErrorHandler = (Error) -> ConnectionErrorAction /** Type for a function that will take in the current HTTP headers and return a new set of HTTP headers to be used when connecting and reconnecting to a stream. */ public typealias HeaderTransform = ([String: String]) -> [String: String] /// Potential actions a `ConnectionErrorHandler` can return public enum ConnectionErrorAction { /** Specifies that the error should be logged normally and dispatched to the `EventHandler`. Connection retrying will proceed normally if appropriate. */ case proceed /** Specifies that the connection should be immediately shut down and not retried. The error will not be dispatched to the `EventHandler` */ case shutdown } /// Struct representing received event from the stream. public struct MessageEvent: Equatable, Hashable { /// The event data of the event. public let data: String /// The last seen event id, or the event id set in the Config if none have been received. public let lastEventId: String /** Constructor for a `MessageEvent` - Parameter data: The `data` field of the `MessageEvent`. - Parameter eventType: The `lastEventId` field of the `MessageEvent`. */ public init(data: String, lastEventId: String = "") { self.data = data self.lastEventId = lastEventId } } /// Protocol for an object that will receive SSE events. public protocol EventHandler { /// EventSource calls this method when the stream connection has been opened. func onOpened() /// EventSource calls this method when the stream connection has been closed. func onClosed() /** EventSource calls this method when it has received a new event from the stream. - Parameter eventType: The type of the event. - Parameter messageEvent: The data for the event. */ func onMessage(eventType: String, messageEvent: MessageEvent) /** EventSource calls this method when it has received a comment line from the stream. - Parameter comment: The comment received. */ func onComment(comment: String) /** This method will be called for all exceptions that occur on the network connection (including an `UnsuccessfulResponseError` if the server returns an unexpected HTTP status), but only after the ConnectionErrorHandler (if any) has processed it. If you need to do anything that affects the state of the connection, use ConnectionErrorHandler. - Parameter error: The error received. */ func onError(error: Error) } /// Enum values representing the states of an EventSource public enum ReadyState: String, Equatable { /// The `EventSource` client has not been started yet. case raw /// The `EventSource` client is attempting to make a connection. case connecting /// The `EventSource` client is active and listening for events. case open /// The connection has been closed or has failed, and the `EventSource` will attempt to reconnect. case closed /// The connection has been permanently closed and the `EventSource` not reconnect. case shutdown } /// Error class that indicates the remote server returned an unsuccessful HTTP response code. public class UnsuccessfulResponseError: Error { /// The HTTP response code received. public let responseCode: Int /** Constructor for an `UnsuccessfulResponseError`. - Parameter responseCode: The HTTP response code of the unsuccessful response. */ public init(responseCode: Int) { self.responseCode = responseCode } } ================================================ FILE: Source/UTF8LineParser.swift ================================================ import Foundation struct DataIter: IteratorProtocol { var data: Data var position: Data.Index { data.startIndex } mutating func next() -> UInt8? { data.popFirst() } } class UTF8LineParser { private let lf = Unicode.Scalar(0x0A) private let cr = Unicode.Scalar(0x0D) private let replacement = String(Unicode.UTF8.decode(Unicode.UTF8.encodedReplacementCharacter)) var utf8Parser = Unicode.UTF8.ForwardParser() var remainder: Data = Data() var currentString: String = "" var seenCr = false func append(_ body: Data) -> [String] { let data = remainder + body var dataIter = DataIter(data: data) var remainderPos = data.endIndex var lines: [String] = [] Decode: while true { switch utf8Parser.parseScalar(from: &dataIter) { case .valid(let scalarResult): let scalar = Unicode.UTF8.decode(scalarResult) if seenCr && scalar == lf { seenCr = false continue } seenCr = scalar == cr if scalar == cr || scalar == lf { lines.append(currentString) currentString = "" } else { currentString.append(String(scalar)) } case .emptyInput: break Decode case .error(let len): seenCr = false if dataIter.position == data.endIndex { // Error at end of block, carry over in case of split code point remainderPos = data.index(data.endIndex, offsetBy: -len) // May as well break here as next will be .emptyInput break Decode } else { // Invalid character, replace with replacement character currentString.append(replacement) } } } remainder = data.subdata(in: remainderPos.. URLSessionConfiguration { let sessionConfig = URLSessionConfiguration.default sessionConfig.protocolClasses = [MockingProtocol.self] + (sessionConfig.protocolClasses ?? []) return sessionConfig } #if !os(Linux) && !os(Windows) func testStartDefaultRequest() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() XCTAssertEqual(handler.request.url, config.url) XCTAssertEqual(handler.request.httpMethod, config.method) XCTAssertEqual(handler.request.httpBody, config.body) XCTAssertEqual(handler.request.timeoutInterval, config.idleTimeout) XCTAssertEqual(handler.request.allHTTPHeaderFields?["Accept"], "text/event-stream") XCTAssertEqual(handler.request.allHTTPHeaderFields?["Cache-Control"], "no-cache") XCTAssertNil(handler.request.allHTTPHeaderFields?["Last-Event-Id"]) es.stop() } func testStartRequestWithConfiguration() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() config.method = "REPORT" config.body = Data("test body".utf8) config.idleTimeout = 500.0 config.lastEventId = "abc" config.headers = ["X-LD-Header": "def"] let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() XCTAssertEqual(handler.request.url, config.url) XCTAssertEqual(handler.request.httpMethod, config.method) XCTAssertEqual(handler.request.bodyStreamAsData(), config.body) XCTAssertEqual(handler.request.timeoutInterval, config.idleTimeout) XCTAssertEqual(handler.request.allHTTPHeaderFields?["Accept"], "text/event-stream") XCTAssertEqual(handler.request.allHTTPHeaderFields?["Cache-Control"], "no-cache") XCTAssertEqual(handler.request.allHTTPHeaderFields?["Last-Event-Id"], config.lastEventId) XCTAssertEqual(handler.request.allHTTPHeaderFields?["X-LD-Header"], "def") es.stop() } func testStartRequestIsNotReentrant() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() let es = EventSource(config: config) es.start() es.start() _ = MockingProtocol.requested.expectEvent() MockingProtocol.requested.expectNoEvent() es.stop() } func testSuccessfulResponseOpens() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() handler.respond(statusCode: 200) XCTAssertEqual(mockHandler.events.expectEvent(), .opened) es.stop() XCTAssertEqual(mockHandler.events.expectEvent(), .closed) } func testLastEventIdUpdatedByEvents() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() config.reconnectTime = 0.1 let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() handler.respond(statusCode: 200) XCTAssertEqual(mockHandler.events.expectEvent(), .opened) XCTAssertEqual(es.getLastEventId(), "") handler.respond(didLoad: "id: abc\n\n") // Comment used for synchronization handler.respond(didLoad: ":comment\n") XCTAssertEqual(mockHandler.events.expectEvent(), .comment("comment")) XCTAssertEqual(es.getLastEventId(), "abc") handler.finish() XCTAssertEqual(mockHandler.events.expectEvent(), .closed) // Expect to reconnect and include new event id let reconnectHandler = MockingProtocol.requested.expectEvent() XCTAssertEqual(reconnectHandler.request.allHTTPHeaderFields?["Last-Event-Id"], "abc") es.stop() } func testUsesRetryTime() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() // Long enough to cause a timeout if the retry time is not updated config.reconnectTime = 5 let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() handler.respond(statusCode: 200) XCTAssertEqual(mockHandler.events.expectEvent(), .opened) handler.respond(didLoad: "retry: 100\n\n") handler.finish() XCTAssertEqual(mockHandler.events.expectEvent(), .closed) // Expect to reconnect before this times out _ = MockingProtocol.requested.expectEvent() es.stop() } func testCallsHandlerWithMessage() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() handler.respond(statusCode: 200) XCTAssertEqual(mockHandler.events.expectEvent(), .opened) handler.respond(didLoad: "event: custom\ndata: {}\n\n") XCTAssertEqual(mockHandler.events.expectEvent(), .message("custom", MessageEvent(data: "{}"))) es.stop() XCTAssertEqual(mockHandler.events.expectEvent(), .closed) } func testRetryOnInvalidResponseCode() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() config.reconnectTime = 0.1 let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() handler.respond(statusCode: 400) guard case let .error(err) = mockHandler.events.expectEvent(), let responseErr = err as? UnsuccessfulResponseError else { XCTFail("Expected UnsuccessfulResponseError to be given to handler") return } XCTAssertEqual(responseErr.responseCode, 400) // Expect the client to reconnect _ = MockingProtocol.requested.expectEvent() es.stop() } func testShutdownByErrorHandlerOnInitialErrorResponse() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() config.reconnectTime = 0.1 config.connectionErrorHandler = { err in if let responseErr = err as? UnsuccessfulResponseError { XCTAssertEqual(responseErr.responseCode, 400) } else { XCTFail("Expected UnsuccessfulResponseError to be given to handler") } return .shutdown } let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() handler.respond(statusCode: 400) // Expect the client not to reconnect MockingProtocol.requested.expectNoEvent(within: 1.0) es.stop() // Error should not have been given to the handler mockHandler.events.expectNoEvent() } func testShutdownByErrorHandlerOnResponseCompletionError() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() config.reconnectTime = 0.1 config.connectionErrorHandler = { _ in .shutdown } let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() handler.respond(statusCode: 200) XCTAssertEqual(mockHandler.events.expectEvent(), .opened) handler.finishWith(error: DummyError()) XCTAssertEqual(mockHandler.events.expectEvent(), .closed) // Expect the client not to reconnect MockingProtocol.requested.expectNoEvent(within: 1.0) es.stop() // Error should not have been given to the handler mockHandler.events.expectNoEvent() } func testShutdownBy204Response() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() config.reconnectTime = 0.1 let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() handler.respond(statusCode: 204) MockingProtocol.requested.expectNoEvent(within: 1.0) es.stop() // Error should not have been given to the handler mockHandler.events.expectNoEvent() } func testCanOverride204DefaultBehavior() { var config = EventSource.Config(handler: mockHandler, url: URL(string: "http://example.com")!) config.urlSessionConfiguration = sessionWithMockProtocol() config.reconnectTime = 0.1 config.connectionErrorHandler = { err in if let responseErr = err as? UnsuccessfulResponseError { XCTAssertEqual(responseErr.responseCode, 204) } else { XCTFail("Expected UnsuccessfulResponseError to be given to handler") } return .shutdown } let es = EventSource(config: config) es.start() let handler = MockingProtocol.requested.expectEvent() handler.respond(statusCode: 204) // Expect the client not to reconnect MockingProtocol.requested.expectNoEvent(within: 1.0) es.stop() // Error should not have been given to the handler mockHandler.events.expectNoEvent() } #endif } private class DummyError: Error { } ================================================ FILE: Tests/MockHandler.swift ================================================ @testable import LDSwiftEventSource enum ReceivedEvent: Equatable { case opened, closed, message(String, MessageEvent), comment(String), error(Error) static func == (lhs: ReceivedEvent, rhs: ReceivedEvent) -> Bool { switch (lhs, rhs) { case (.opened, .opened): return true case (.closed, .closed): return true case let (.message(typeLhs, eventLhs), .message(typeRhs, eventRhs)): return typeLhs == typeRhs && eventLhs == eventRhs case let (.comment(lhs), .comment(rhs)): return lhs == rhs case (.error, .error): return true default: return false } } } class MockHandler: EventHandler { var events = EventSink() func onOpened() { events.record(.opened) } func onClosed() { events.record(.closed) } func onMessage(eventType: String, messageEvent: MessageEvent) { events.record(.message(eventType, messageEvent)) } func onComment(comment: String) { events.record(.comment(comment)) } func onError(error: Error) { events.record(.error(error)) } } ================================================ FILE: Tests/TestUtil.swift ================================================ import XCTest #if os(Linux) || os(Windows) import FoundationNetworking #endif struct EventSink { private let semaphore = DispatchSemaphore(value: 0) private let queue = DispatchQueue(label: "EventSinkQueue." + UUID().uuidString) var receivedEvents: [T] = [] mutating func record(_ event: T) { queue.sync { receivedEvents.append(event) } semaphore.signal() } mutating func expectEvent(maxWait: TimeInterval = 1.0) -> T { switch semaphore.wait(timeout: DispatchTime.now() + maxWait) { case .success: return queue.sync { receivedEvents.remove(at: 0) } case .timedOut: XCTFail("Expected mock handler to be called") return (nil as T?)! } } mutating func maybeEvent() -> T? { switch semaphore.wait(timeout: DispatchTime.now()) { case .success: return queue.sync { receivedEvents.remove(at: 0) } case .timedOut: return nil } } func expectNoEvent(within: TimeInterval = 0.1) { if case .success = semaphore.wait(timeout: DispatchTime.now() + within) { XCTFail("Expected no events in sink, found \(String(describing: receivedEvents.first))") } } } class RequestHandler { let proto: URLProtocol let request: URLRequest let client: URLProtocolClient? var stopped = false init(proto: URLProtocol, request: URLRequest, client: URLProtocolClient?) { self.proto = proto self.request = request self.client = client } func respond(statusCode: Int) { let headers = ["Content-Type": "text/event-stream; charset=utf-8", "Transfer-Encoding": "chunked"] let resp = HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: headers)! client?.urlProtocol(proto, didReceive: resp, cacheStoragePolicy: .notAllowed) } func respond(didLoad: String) { respond(didLoad: Data(didLoad.utf8)) } func respond(didLoad: Data) { client?.urlProtocol(proto, didLoad: didLoad) } func finishWith(error: Error) { client?.urlProtocol(proto, didFailWithError: error) } func finish() { client?.urlProtocolDidFinishLoading(proto) } func stop() { stopped = true } } class MockingProtocol: URLProtocol { override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override class func canInit(with request: URLRequest) -> Bool { true } override class func canInit(with task: URLSessionTask) -> Bool { true } static var requested = EventSink() class func resetRequested() { requested = EventSink() } private var currentlyLoading: RequestHandler? override func startLoading() { let handler = RequestHandler(proto: self, request: request, client: client) currentlyLoading = handler MockingProtocol.requested.record(handler) } override func stopLoading() { currentlyLoading?.stop() currentlyLoading = nil } } extension URLRequest { func bodyStreamAsData() -> Data? { guard let bodyStream = self.httpBodyStream else { return nil } bodyStream.open() defer { bodyStream.close() } let bufSize: Int = 16 let buf = UnsafeMutablePointer.allocate(capacity: bufSize) defer { buf.deallocate() } var data = Data() while bodyStream.hasBytesAvailable { let readDat = bodyStream.read(buf, maxLength: bufSize) data.append(buf, count: readDat) } return data } } ================================================ FILE: Tests/UTF8LineParserTests.swift ================================================ import XCTest @testable import LDSwiftEventSource final class UTF8LineParserTests: XCTestCase { var parser = UTF8LineParser() override func setUp() { super.setUp() parser = UTF8LineParser() } override func tearDown() { super.tearDown() // Validate that `closeAndReset` completely resets the parser parser.closeAndReset() XCTAssertEqual(parser.append(Data("\n".utf8)), [""]) } // swiftlint:disable:next empty_xctest_method - Only runs test in tearDown func testNoData() { } func testEmptyData() { XCTAssertEqual(parser.append(Data()), []) } func testEmptyCrLine() { XCTAssertEqual(parser.append(Data("\r".utf8)), [""]) } func testBasicLineUnterminated() { let line = "test string" XCTAssertEqual(parser.append(Data(line.utf8)), []) } func testBasicLineCr() { let line = "test string" let data = Data((line + "\r").utf8) XCTAssertEqual(parser.append(data), [line]) } func testBasicLineLf() { let line = "test string" let data = Data((line + "\n").utf8) XCTAssertEqual(parser.append(data), [line]) } func testBasicLineCrLf() { let line = "test string" let data = Data((line + "\r\n").utf8) XCTAssertEqual(parser.append(data), [line]) } func testBasicSplit() { XCTAssertEqual(parser.append(Data("test ".utf8)), []) XCTAssertEqual(parser.append(Data("string\r".utf8)), ["test string"]) } func testUnicodeString() { let line = "¯\\_(ツ)_/¯0️⃣🇺🇸Z̮̞̠͙͔ͅḀ̗̞͈̻̗Ḷ͙͎̯̹̞͓G̻O̭̗̮𝓯𝓸𝔁" XCTAssertEqual(parser.append(Data((line + "\n").utf8)), [line]) } func testNullCodePoint() { let line = "\u{0000}" XCTAssertEqual(parser.append(Data((line + "\n").utf8)), [line]) } func testInvalidCharacterReplaced() { let line = "test✨string" var data = Data((line + "\n").utf8) // Remove 3rd and last byte of "✨" data.remove(at: 6) let expected = "test�string" XCTAssertEqual(parser.append(data), [expected]) } // Simulates a multi-code-unit code point being split across received chunks from the network. func testCodePointSplitNotReplaced() { let line = "test✨string" let data = Data((line + "\r").utf8) let data1 = data.subdata(in: 0..<6) let data2 = data.subdata(in: 6..<14) XCTAssertEqual(parser.append(data1), []) XCTAssertEqual(parser.append(data2), [line]) } // Simulates the stream dropping part way through a multi-code-unit code point. func testResetAfterPartialInvalid() { var data = Data("test✨".utf8) data.remove(at: 6) XCTAssertEqual(parser.append(data), []) } func testInvalidCharacterReplacedOnNextLineAfterCr() { let line = "test\r✨string\r" var data = Data(line.utf8) // Remove 3rd and last byte of "✨" data.remove(at: 7) XCTAssertEqual(parser.append(data), ["test", "�string"]) } func testMultiLineDataMixedLineEnding() { let line = "test1\rtest2\ntest3\r\ntest4\r\rtest5\n\n" let data = Data(line.utf8) let expected = ["test1", "test2", "test3", "test4", "", "test5", ""] XCTAssertEqual(parser.append(data), expected) } } ================================================ FILE: release-please-config.json ================================================ { "packages": { ".": { "release-type": "simple", "bump-minor-pre-major": true, "versioning": "default", "include-v-in-tag": false, "include-component-in-tag": false, "extra-files": [ "LDSwiftEventSource.podspec", "README.md" ] } } }