Skip to content

Commit eb2b8b5

Browse files
committed
Add shareReplay operator
1 parent 31462b0 commit eb2b8b5

17 files changed

+4621
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.swiftpm

README.md

+40
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ This repo contains XCombine, a Swift module, developed on top of the Combine fra
1616
- [Zip Operator](#zip-operator)
1717
- [General Structure](#general-structure)
1818
- [Example of Usage](#example-of-usage)
19+
- [ShareReplay Operator](#sharereplay-operator)
20+
- [Example of Usage](#example-of-usage-sharereplay)
1921
- [License](#license)
2022

2123
## Installation
@@ -97,8 +99,46 @@ events.send("foo")
9799
events.send("bar")
98100
```
99101

102+
## ShareReplay Operator
103+
104+
XCombine offers the `shareReplay` operator that is not yet present in the Combine framework. It returns a publisher that shares a single subscription to the upstream. It also buffers a given number of latest incoming stream values and immediately upon subscription replays them. XCombine's `shareReplay` operator features:
105+
106+
* autoconnect (reference counting) mechanism
107+
108+
* a circular buffer for caching-related optimization
109+
110+
* a fine grained back pressure handling
111+
112+
### <a name="example-of-usage-sharereplay"></a>Example of Usage
113+
114+
The following snippet is the updated example from the [blog post][combine-sharereplay-operator] on developing your own `shareReplay` operator. It demonstrates the use of XCombine's `shareReplay` operator.
115+
116+
```swift
117+
import Combine
118+
import XCombine
119+
120+
let measurements = PassthroughSubject<Int, Never>()
121+
122+
let diagramDataSource = measurements
123+
    .share(replay: 3)
124+
125+
let subscriber1 = diagramDataSource
126+
    .sink(
127+
        receiveCompletion: { completion in
128+
            print("Subscriber 1:", completion)
129+
        },
130+
        receiveValue: { temperature in
131+
            print("Subscriber 1:", temperature)
132+
        }
133+
    )
134+
135+
measurements.send(100)
136+
measurements.send(110)
137+
```
138+
100139
## License
101140

102141
This project is licensed under the MIT license.
103142

104143
[combine-insight-into-zip-operator]: https://sergebouts.github.io/combine-insight-into-zip-operator/
144+
[combine-sharereplay-operator]: https://sergebouts.github.io/combine-sharereplay-operator/

Sources/XCombine/CircularBuffer.swift

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//
2+
// CircularBuffer.swift
3+
// XCombine
4+
//
5+
// Created by Serge Bouts on 10/12/19.
6+
// Copyright © 2019 Serge Bouts. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// Circular buffer errors.
12+
enum CircularBufferError: Error, Equatable {
13+
case invalidCapacity
14+
case overflow
15+
case isEmpty
16+
case outOfRange
17+
}
18+
19+
/// A circular buffer implementation.
20+
///
21+
/// See also: [Circular buffer](https://en.wikipedia.org/wiki/Circular_buffer)
22+
struct CircularBuffer<Element> {
23+
// MARK: - Properties
24+
25+
private var data: [Element]
26+
27+
private var head: Int = 0
28+
29+
private let lock = NSLock()
30+
31+
// MARK: - Initialization
32+
33+
/// Creates an instance with the buffer of `capacity` elements size.
34+
///
35+
/// - Parameter capacity: The buffer's capacity.
36+
/// - Throws: `CircularBufferError.invalidCapacity` if the capacity value is wrong.
37+
public init(capacity: Int) throws {
38+
guard capacity > 0 else { throw CircularBufferError.invalidCapacity }
39+
40+
self.capacity = capacity
41+
self.data = []
42+
// `Int.max` capacity value is a special case, for which we don't reserve capacity at all.
43+
if capacity < Int.max {
44+
data.reserveCapacity(capacity)
45+
}
46+
}
47+
48+
// MARK: - API
49+
50+
/// The buffer's capacity.
51+
private(set) var capacity: Int
52+
53+
/// The buffer's current size.
54+
private(set) var count = 0
55+
56+
/// Returns the index'th element if the index is not out of range;
57+
/// returns `nil` otherwise.
58+
subscript(safe index: Int) -> Element? {
59+
lock.lock()
60+
defer { lock.unlock() }
61+
62+
guard index >= 0 && index < count else { return nil }
63+
64+
let index = (head + index) % capacity
65+
66+
return data[index]
67+
}
68+
69+
/// Returns the index'th element if the index is correct;
70+
/// throws otherwise.
71+
///
72+
/// - Parameter index: The element's index.
73+
/// - Throws: `CircularBufferError.outOfRange` if the index is out of range.
74+
/// - Returns: An element if the index is correct.
75+
func get(at index: Int) throws -> Element {
76+
guard let result = self[safe: index] else { throw CircularBufferError.outOfRange }
77+
return result
78+
}
79+
80+
/// Appends an element at the end of the buffer if the buffer is not full;
81+
/// throws otherwise.
82+
///
83+
/// - Parameter element: The element to append.
84+
/// - Throws: `CircularBufferError.overflow` if the buffer if full.
85+
mutating func append(_ element: Element) throws {
86+
lock.lock()
87+
defer { lock.unlock() }
88+
89+
guard !isFull else { throw CircularBufferError.overflow }
90+
91+
if data.count < capacity {
92+
data.append(element)
93+
} else {
94+
data[(head + count) % capacity] = element
95+
}
96+
97+
count += 1
98+
}
99+
100+
/// Removes the first element from the buffer if the buffer is not empty;
101+
/// throws otherwise.
102+
///
103+
/// - Throws: `CircularBufferError.isEmpty` if the buffer is empty.
104+
mutating func removeFirst() throws {
105+
lock.lock()
106+
defer { lock.unlock() }
107+
108+
guard count > 0 else { throw CircularBufferError.isEmpty }
109+
110+
head = (head + 1) % capacity
111+
count -= 1
112+
}
113+
114+
/// Returns `true` if the buffer if empty;
115+
/// `false` otherwise.
116+
var isEmpty: Bool {
117+
count == 0
118+
}
119+
120+
/// Returns `true` if the buffer if full;
121+
/// `false` otherwise.
122+
var isFull: Bool {
123+
assert(count <= capacity)
124+
return count == capacity
125+
}
126+
127+
/// Returns the number of elements, that can yet be appended.
128+
var freeSpace: Int {
129+
return capacity - count
130+
}
131+
}

0 commit comments

Comments
 (0)