diff --git a/.github/workflows/Checks.yml b/.github/workflows/Checks.yml index 2622d8b..66fa0aa 100644 --- a/.github/workflows/Checks.yml +++ b/.github/workflows/Checks.yml @@ -7,49 +7,27 @@ on: branches: "*" jobs: - pod-lint: - runs-on: macos-12 - - steps: - - uses: maxim-lobanov/setup-xcode@v1.1 - with: - xcode-version: "13.3" - - uses: actions/checkout@v2 - - name: Run lint - run: pod lib lint --allow-warnings - build-xcodebuild: - runs-on: macos-12 + runs-on: macos-14 steps: - uses: maxim-lobanov/setup-xcode@v1.1 with: - xcode-version: "13.3" + xcode-version: "16.0" - uses: actions/checkout@v2 - name: xcodebuild - run: xcodebuild -scheme BulkLogger -sdk iphoneos -destination 'generic/platform=iOS' - - build-swiftpm: - runs-on: macos-12 - - steps: - - uses: maxim-lobanov/setup-xcode@v1.1 - with: - xcode-version: "13.3" - - uses: actions/checkout@v2 - - name: SwiftPM - run: swift build + run: set -o pipefail && xcodebuild -scheme Bulk-Package -sdk iphoneos -destination 'generic/platform=iOS' | xcbeautify test: - runs-on: macos-12 + runs-on: macos-14 steps: - uses: maxim-lobanov/setup-xcode@v1.1 with: - xcode-version: "13.3" + xcode-version: "16.0" - uses: actions/checkout@v2 with: submodules: true - name: Run run: | - swift test + set -o pipefail && xcodebuild -scheme Bulk-Package -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 16 Pro' test | xcbeautify diff --git a/Bulk.podspec b/Bulk.podspec deleted file mode 100644 index 132fe48..0000000 --- a/Bulk.podspec +++ /dev/null @@ -1,26 +0,0 @@ -Pod::Spec.new do |s| - s.name = "Bulk" - s.version = "0.7.0" - s.summary = "Bulk is a library for buffering the objects. Pipeline(Sink) receives the object and emits the object bulked." - s.homepage = "https://github.com/muukii/Bulk" - s.license = 'MIT' - s.author = { "muukii" => "muukii.app@gmail.com" } - s.source = { :git => "https://github.com/muukii/Bulk.git", :tag => s.version } - s.social_media_url = 'https://twitter.com/muukii_app' - - s.platform = :ios, '10.0' - s.requires_arc = true - - s.default_subspec = 'Bulk' - s.swift_version = '5.6' - s.weak_frameworks = ['Combine'] - - s.subspec 'Bulk' do |ss| - ss.source_files = 'Sources/Bulk/**/*.swift' - end - - s.subspec 'BulkLogger' do |ss| - ss.source_files = 'Sources/BulkLogger/**/*.swift' - ss.dependency 'Bulk/Bulk' - end -end diff --git a/Package.swift b/Package.swift index a8fffcf..35ad03e 100644 --- a/Package.swift +++ b/Package.swift @@ -1,20 +1,19 @@ -// swift-tools-version:5.6 +// swift-tools-version:6.0 import PackageDescription let package = Package( - name: "Bulk", + name: "Bulk", platforms: [ .macOS(.v11), - .iOS(.v13), + .iOS(.v16), .tvOS(.v13), - .watchOS(.v5) + .watchOS(.v6), ], products: [ .library(name: "Bulk", targets: ["Bulk"]), .library(name: "BulkLogger", targets: ["BulkLogger"]), ], - dependencies: [ - ], + dependencies: [], targets: [ .target(name: "Bulk", dependencies: []), .target(name: "BulkLogger", dependencies: ["Bulk"]), diff --git a/Sources/Bulk/Core/BulkSinkGroup.swift b/Sources/Bulk/Buffers/Buffer.swift similarity index 69% rename from Sources/Bulk/Core/BulkSinkGroup.swift rename to Sources/Bulk/Buffers/Buffer.swift index 755c69d..61ebf2e 100644 --- a/Sources/Bulk/Core/BulkSinkGroup.swift +++ b/Sources/Bulk/Buffers/Buffer.swift @@ -21,22 +21,25 @@ import Foundation -public final class BulkSinkGroup: BulkSinkType { - - private let sinks: AnyCollection> - - public init(_ sink: Sink) where Sink.Element == Element { - self.sinks = AnyCollection(CollectionOfOne>.init(.init(sink))) - } +public enum BufferResult { + case stored + case flowed([Element]) +} + +public protocol Buffer { + + associatedtype Element - public init(_ sinks: C) where C.Element == AnyBulkSink { - self.sinks = AnyCollection(sinks) - } + var hasSpace: Bool { get } - public func send(_ element: Element) { - sinks.forEach { - $0.send(element) - } - } + /// Buffer item + /// + /// - Parameter string: + /// - Returns: + func write(element: Element) -> BufferResult + /// Purge buffered items + /// + /// - Returns: purged items + func purge() -> [Element] } diff --git a/Sources/Bulk/Buffers/FileBuffer.swift b/Sources/Bulk/Buffers/FileBuffer.swift index 30dd68e..37a6867 100644 --- a/Sources/Bulk/Buffers/FileBuffer.swift +++ b/Sources/Bulk/Buffers/FileBuffer.swift @@ -21,14 +21,16 @@ import Foundation -public final class FileBuffer: BufferType where Serializer.Element == Element { - +public final class FileBuffer: Buffer where Serializer.Element == Element { + public var hasSpace: Bool { return lineCount() < size } - private let fileManager = FileManager.default + nonisolated(unsafe) private let fileManager = FileManager.default public let fileURL: URL + + nonisolated(unsafe) private var fileHandle: FileHandle? public let size: Int diff --git a/Sources/Bulk/Buffers/MemoryBuffer.swift b/Sources/Bulk/Buffers/MemoryBuffer.swift index c18823f..d4e3798 100644 --- a/Sources/Bulk/Buffers/MemoryBuffer.swift +++ b/Sources/Bulk/Buffers/MemoryBuffer.swift @@ -21,7 +21,7 @@ import Foundation -public final class MemoryBuffer: BufferType { +public final class MemoryBuffer: Buffer { public var hasSpace: Bool { return cursor < size diff --git a/Sources/Bulk/Buffers/PassthroughBuffer.swift b/Sources/Bulk/Buffers/PassthroughBuffer.swift index 7fda21f..774021f 100644 --- a/Sources/Bulk/Buffers/PassthroughBuffer.swift +++ b/Sources/Bulk/Buffers/PassthroughBuffer.swift @@ -23,7 +23,7 @@ import Foundation -public struct PassthroughBuffer: BufferType { +public struct PassthroughBuffer: Buffer { public var hasSpace: Bool { return false diff --git a/Sources/Bulk/Buffers/SerializerType.swift b/Sources/Bulk/Buffers/SerializerType.swift index 484812c..07db6c2 100644 --- a/Sources/Bulk/Buffers/SerializerType.swift +++ b/Sources/Bulk/Buffers/SerializerType.swift @@ -21,7 +21,7 @@ import Foundation -public protocol SerializerType { +public protocol SerializerType: Sendable { associatedtype Element diff --git a/Sources/Bulk/Core/Buffer.swift b/Sources/Bulk/Core/Buffer.swift deleted file mode 100644 index 23a8228..0000000 --- a/Sources/Bulk/Core/Buffer.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// Copyright (c) 2020 Hiroshi Kimura(Muukii) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -public enum BufferResult { - case stored - case flowed([Element]) -} - -public protocol BufferType { - - associatedtype Element - - /// - var hasSpace: Bool { get } - - /// Buffer item - /// - /// - Parameter string: - /// - Returns: - func write(element: Element) -> BufferResult - - /// Purge buffered items - /// - /// - Returns: purged items - func purge() -> [Element] -} - -extension BufferType { - - public func asAny() -> AnyBuffer { - .init(backing: self) - } -} - -public struct AnyBuffer: BufferType { - - private let _hasSpace: () -> Bool - private let _purge: () -> [Element] - private let _write: (_ element: Element) -> BufferResult - - public init(backing: Buffer) where Buffer.Element == Element { - self._hasSpace = { - backing.hasSpace - } - self._purge = backing.purge - self._write = backing.write - } - - public var hasSpace: Bool { - _hasSpace() - } - - public func write(element: Element) -> BufferResult { - _write(element) - } - - public func purge() -> [Element] { - _purge() - } - -} diff --git a/Sources/Bulk/Core/BulkBufferTimer.swift b/Sources/Bulk/Core/BulkBufferTimer.swift index ddfb737..a563591 100644 --- a/Sources/Bulk/Core/BulkBufferTimer.swift +++ b/Sources/Bulk/Core/BulkBufferTimer.swift @@ -1,60 +1,42 @@ import Foundation -public struct BulkBufferTimer { +public final class BulkBufferTimer { - private let interval: DispatchTimeInterval + private var interval: Duration - private let onTimeout: @Sendable () async -> Void + private var onTimeout: (isolated (any Actor)?) async -> Void private var item: Task<(), Never>? public init( - interval: DispatchTimeInterval, - @_inheritActorContext onTimeout: @escaping @Sendable () async -> Void + interval: Duration, + onTimeout: sending @escaping () async -> Void ) { self.interval = interval - self.onTimeout = onTimeout - - refresh() + self.onTimeout = { a in + await onTimeout() + } + } - public mutating func tap() { - refresh() + public func tap(isolation: isolated (any Actor)? = #isolation) { + refresh(isolation: isolation) } - private mutating func refresh() { + private func refresh(isolation: isolated (any Actor)? = #isolation) { self.item?.cancel() let task = Task { [onTimeout, interval] in - try? await Task.sleep(nanoseconds: interval.makeNanoseconds()) + try? await Task.sleep(for: interval) guard Task.isCancelled == false else { return } - await onTimeout() + await onTimeout(isolation) } self.item = task } } - -extension DispatchTimeInterval { - - fileprivate func makeNanoseconds() -> UInt64 { - - switch self { - case .nanoseconds(let v): return UInt64(v) - case .microseconds(let v): return UInt64(v) * 1_000 - case .milliseconds(let v): return UInt64(v) * 1_000_000 - case .seconds(let v): return UInt64(v) * 1_000_000_000 - case .never: return UInt64.max - @unknown default: - assertionFailure() - return 0 - } - - } - -} diff --git a/Sources/Bulk/Core/BulkSink.swift b/Sources/Bulk/Core/BulkSink.swift index 5995ba1..bb48cff 100644 --- a/Sources/Bulk/Core/BulkSink.swift +++ b/Sources/Bulk/Core/BulkSink.swift @@ -1,45 +1,37 @@ -// -// BulkSink.swift -// Bulk -// -// Created by muukii on 2020/02/04. -// -import Foundation - -public final class BulkSink: BulkSinkType { - - private let targetQueue = DispatchQueue.init(label: "me.muukii.bulk") +public protocol BulkSinkType: Actor { - private let targets: [AnyTarget] + associatedtype Element - private var timer: BulkBufferTimer! + func send(_ element: Element) +} + +public actor BulkSink: BulkSinkType { + + public typealias Element = B.Element + + private let targets: [any TargetType] + + private let timer: BulkBufferTimer + + private let buffer: B - private let buffer: AnyBuffer - public init( - buffer: AnyBuffer, - debounceDueTime: DispatchTimeInterval = .seconds(10), - targets: [AnyTarget] + buffer: B, + debounceDueTime: Duration = .seconds(1), + targets: [any TargetType] ) { self.buffer = buffer self.targets = targets - - self.timer = BulkBufferTimer(interval: debounceDueTime) { [weak self] in - - guard let self else { return } - - self.targetQueue.async { [self] in - let elements = buffer.purge() - - self.targets.forEach { - $0.write(items: elements) - } + + weak var instance: BulkSink? - self.timer.tap() - } + self.timer = BulkBufferTimer(interval: debounceDueTime) { [instance] in + await instance?.purge() } + + instance = self } @@ -47,26 +39,31 @@ public final class BulkSink: BulkSinkType { } - public func send(_ newElement: Element) { - targetQueue.async { [self] in - switch buffer.write(element: newElement) { - case .flowed(let elements): - // TODO: align interface of Collection - targets.forEach { - $0.write(items: elements) - } - case .stored: - break - } + private func purge() { + let elements = buffer.purge() + elements.forEach { + self.send($0) } } - public func flush() { - targetQueue.async { [self] in - let elements = buffer.purge() + public func send(_ newElement: Element) { + timer.tap() + switch buffer.write(element: newElement) { + case .flowed(let elements): + // TODO: align interface of Collection targets.forEach { $0.write(items: elements) } + case .stored: + break + } + } + + public func flush() { + timer.tap() + let elements = buffer.purge() + targets.forEach { + $0.write(items: elements) } } diff --git a/Sources/Bulk/Core/CombineBulkSink.swift b/Sources/Bulk/Core/CombineBulkSink.swift index afe64e6..58d4a7b 100644 --- a/Sources/Bulk/Core/CombineBulkSink.swift +++ b/Sources/Bulk/Core/CombineBulkSink.swift @@ -21,30 +21,3 @@ import Foundation -public protocol BulkSinkType { - - associatedtype Element - - func send(_ element: Element) -} - -extension BulkSinkType { - - public func asAny() -> AnyBulkSink { - .init(self) - } -} - -public final class AnyBulkSink: BulkSinkType { - - private let _send: (Element) -> Void - - public init(_ source: Sink) where Sink.Element == Element { - self._send = source.send - } - - public func send(_ element: Element) { - _send(element) - } - -} diff --git a/Sources/Bulk/Core/Target.swift b/Sources/Bulk/Core/Target.swift index 9b7c086..ee642de 100644 --- a/Sources/Bulk/Core/Target.swift +++ b/Sources/Bulk/Core/Target.swift @@ -21,29 +21,9 @@ import Foundation -public protocol TargetType { +public protocol TargetType { associatedtype Element func write(items: [Element]) } - -extension TargetType { - - public func asAny() -> AnyTarget { - .init(backing: self) - } -} - -public struct AnyTarget: TargetType { - - private let _write: (_ formatted: [Element]) -> Void - - public init(backing: Target) where Target.Element == Element { - self._write = backing.write - } - - public func write(items: [Element]) { - _write(items) - } -} diff --git a/Sources/Bulk/Core/TargetUmbrella.swift b/Sources/Bulk/Core/TargetUmbrella.swift index a51ddfb..f2bd0e8 100644 --- a/Sources/Bulk/Core/TargetUmbrella.swift +++ b/Sources/Bulk/Core/TargetUmbrella.swift @@ -28,7 +28,7 @@ public struct TargetUmbrella: TargetType { public init( filter: @escaping (Element) -> Bool = { _ in true }, transform: @escaping (Element) -> U, - targets: [AnyTarget] + targets: [any TargetType] ) { self._write = { elements in let results = elements diff --git a/Sources/Bulk/Buffers/CodableSerializer.swift b/Sources/Bulk/Serializer/CodableSerializer.swift similarity index 100% rename from Sources/Bulk/Buffers/CodableSerializer.swift rename to Sources/Bulk/Serializer/CodableSerializer.swift diff --git a/Sources/BulkLogger/LogData.swift b/Sources/BulkLogger/LogData.swift index 391d79c..a9c2340 100644 --- a/Sources/BulkLogger/LogData.swift +++ b/Sources/BulkLogger/LogData.swift @@ -23,7 +23,7 @@ import Foundation -public struct LogData: Codable { +public struct LogData: Codable, Sendable { /// Logging Level /// @@ -32,7 +32,7 @@ public struct LogData: Codable { /// - info: Info /// - warn: Warn /// - error: Error - public enum Level: String, Codable { + public enum Level: String, Codable, Sendable { case verbose case debug case info diff --git a/Sources/BulkLogger/Logger.swift b/Sources/BulkLogger/Logger.swift index eb05e3b..36e95b4 100644 --- a/Sources/BulkLogger/Logger.swift +++ b/Sources/BulkLogger/Logger.swift @@ -40,14 +40,14 @@ public final class Logger { private let lock = NSLock() // MARK: - Properties - - private(set) public var sinks: [AnyBulkSink] - + + private(set) public var sinks: [any BulkSinkType] + public let context: [String] // MARK: - Initializers - - public init(context: String, sinks: [AnyBulkSink]) { + + public init(context: String, sinks: [any BulkSinkType]) { self.context = [context] self.sinks = sinks } @@ -87,9 +87,11 @@ public final class Logger { function: function.description, line: line ) - - sinks.forEach { target in - target.send(log) + + Task { [sinks] in + for sink in sinks { + await sink.send(log) + } } } @@ -158,9 +160,9 @@ public final class Logger { ) { _write(level: .error, items, file: file, function: function, line: line, dsoHandle: dsoHandle) } - - public func addSink(_ sink: Sink) where Sink.Element == LogData { - sinks.append(.init(sink)) + + public func addSink(_ sink: any BulkSinkType) { + sinks.append(sink) } public func makeContextualLogger(context: String) -> Logger { diff --git a/Sources/BulkLogger/Logger/LogFileTarget.swift b/Sources/BulkLogger/Logger/LogFileTarget.swift index 2ecd00e..175bc03 100644 --- a/Sources/BulkLogger/Logger/LogFileTarget.swift +++ b/Sources/BulkLogger/Logger/LogFileTarget.swift @@ -28,7 +28,7 @@ open class LogFileTarget: TargetType { public let fileURL: URL private var fileHandle: FileHandle? - private let lock = NSRecursiveLock() + private let lock = NSLock() public init(filePath: String) { diff --git a/Tests/BulkTests/BasicTests.swift b/Tests/BulkTests/BasicTests.swift index 1dc68f9..b0361e8 100644 --- a/Tests/BulkTests/BasicTests.swift +++ b/Tests/BulkTests/BasicTests.swift @@ -20,17 +20,43 @@ final class BasicTests: XCTestCase { } } - func testSimple() { + func testNoBuffer() async { - let sink = BulkSink( - buffer: MemoryBuffer.init(size: 1).asAny(), + let sink = BulkSink>.init( + buffer: .init(), targets: [ - MyTarget().asAny() + MyTarget() ] ) + + await sink.send("A") + await sink.send("A") + await sink.send("A") - sink.send("A") - sink.send("B") - sink.send("C") +// sink.send("A") +// sink.send("B") +// sink.send("C") + } + + + func testMemoryBuffer() async { + + let sink = BulkSink>.init( + buffer: .init(size: 10), + targets: [ + MyTarget() + ] + ) + + await sink.send("A") + await sink.send("A") + await sink.send("A") + + try? await Task.sleep(for: .seconds(3)) + + // sink.send("A") + // sink.send("B") + // sink.send("C") } + } diff --git a/Tests/BulkTests/TimerTests.swift b/Tests/BulkTests/TimerTests.swift index 24f4e2c..e568722 100644 --- a/Tests/BulkTests/TimerTests.swift +++ b/Tests/BulkTests/TimerTests.swift @@ -14,29 +14,22 @@ import XCTest class TimerTests: XCTestCase { - func testTimer() { - - let g = DispatchGroup() - - g.enter() + func testTimer() async { var count: Int = 0 - let timer = Bulk.BulkBufferTimer(interval: .milliseconds(100), queue: .global()) { + let timer = Bulk.BulkBufferTimer(interval: .milliseconds(100)) { count += 1 - g.leave() } - timer.tap() - Thread.sleep(forTimeInterval: 0.01) - timer.tap() - Thread.sleep(forTimeInterval: 0.09) - timer.tap() - - g.wait() - - Thread.sleep(forTimeInterval: 1) - + await timer.tap() + try? await Task.sleep(for: .milliseconds(10)) + await timer.tap() + try? await Task.sleep(for: .milliseconds(10)) + await timer.tap() + + try? await Task.sleep(for: .milliseconds(1000)) + XCTAssert(count == 1) } }