Skip to content

Commit 61ae43f

Browse files
wip public API
Based on JS at 0b4b0f8. The entry point is the DefaultChatClient class. Chose to use existential types when one protocol returns another (i.e. `-> any MyProtocol`) instead of associated types, because it’s more readable and I don’t think that we need to worry about the performance implications. turned on explicit_acl, but just for the library — it's a handy thing to have to make sure you've not missed making anything public, but we don’t need it for BuildTool (For future reference, generate memberwise initializers in Xcode by clicking at the start of the type declaration and doing Editor -> Refactor -> Generate Memberwise Initializer.) Skipped copying the docstrings from JS; created #1 to do this later. Turned off missing_docs for now. We’ll be able to validate this API when we do #4, which will create a mock implementation of this API and then build the example app around the mock. Some stuff is up in the air until we start trying to implement, too. maybe have been too liberal with Sendable not sure if the types where `subscribe()` just returns a subscription could be sequences themselves, i still don't understand the nuance of buffering and whatnot, what happens if you share them so the api decided on is that there are no responses, you can directly iterate over what comes out (because there’s no need for an `off()`)
1 parent 8c7b0a4 commit 61ae43f

27 files changed

+562
-10
lines changed

.swiftlint.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ opt_in_rules:
7777
# Opt-in rules of type "lint" that we’ve decided we want:
7878
- array_init
7979
- empty_xctest_method
80-
- missing_docs
8180
- override_in_extension
8281
- yoda_condition
8382
- private_swiftui_state

Example/AblyChatExample.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
212F95A72C6CAD9300420287 /* MockRealtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212F95A62C6CAD9300420287 /* MockRealtime.swift */; };
1011
21971DFF2C60D89C0074B8AE /* AblyChat in Frameworks */ = {isa = PBXBuildFile; productRef = 21971DFE2C60D89C0074B8AE /* AblyChat */; };
1112
21F09AA02C60CAF00025AF73 /* AblyChatExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F09A9F2C60CAF00025AF73 /* AblyChatExampleApp.swift */; };
1213
21F09AA22C60CAF00025AF73 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21F09AA12C60CAF00025AF73 /* ContentView.swift */; };
@@ -15,6 +16,7 @@
1516
/* End PBXBuildFile section */
1617

1718
/* Begin PBXFileReference section */
19+
212F95A62C6CAD9300420287 /* MockRealtime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRealtime.swift; sourceTree = "<group>"; };
1820
21F09A9C2C60CAF00025AF73 /* AblyChatExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AblyChatExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
1921
21F09A9F2C60CAF00025AF73 /* AblyChatExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AblyChatExampleApp.swift; sourceTree = "<group>"; };
2022
21F09AA12C60CAF00025AF73 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -35,6 +37,14 @@
3537
/* End PBXFrameworksBuildPhase section */
3638

3739
/* Begin PBXGroup section */
40+
212F95A52C6CAD7E00420287 /* Mocks */ = {
41+
isa = PBXGroup;
42+
children = (
43+
212F95A62C6CAD9300420287 /* MockRealtime.swift */,
44+
);
45+
path = Mocks;
46+
sourceTree = "<group>";
47+
};
3848
21971DFD2C60D89C0074B8AE /* Frameworks */ = {
3949
isa = PBXGroup;
4050
children = (
@@ -62,6 +72,7 @@
6272
21F09A9E2C60CAF00025AF73 /* AblyChatExample */ = {
6373
isa = PBXGroup;
6474
children = (
75+
212F95A52C6CAD7E00420287 /* Mocks */,
6576
21F09A9F2C60CAF00025AF73 /* AblyChatExampleApp.swift */,
6677
21F09AA12C60CAF00025AF73 /* ContentView.swift */,
6778
21F09AA32C60CAF20025AF73 /* Assets.xcassets */,
@@ -152,6 +163,7 @@
152163
isa = PBXSourcesBuildPhase;
153164
buildActionMask = 2147483647;
154165
files = (
166+
212F95A72C6CAD9300420287 /* MockRealtime.swift in Sources */,
155167
21F09AA22C60CAF00025AF73 /* ContentView.swift in Sources */,
156168
21F09AA02C60CAF00025AF73 /* AblyChatExampleApp.swift in Sources */,
157169
);

Example/AblyChatExample/ContentView.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import SwiftUI
33

44
struct ContentView: View {
55
/// Just used to check that we can successfully import and use the AblyChat library. TODO remove this once we start building the library
6-
@State private var ablyChatClient = AblyChatClient()
6+
@State private var ablyChatClient = DefaultChatClient(
7+
realtime: MockRealtime(key: ""),
8+
clientOptions: ClientOptions()
9+
)
710

811
var body: some View {
912
VStack {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Ably
2+
3+
/// A mock implementation of `ARTRealtimeProtocol`. It only exists so that we can construct an instance of `DefaultChatClient` without needing to create a proper `ARTRealtime` instance (which we can’t yet do because we don’t have a method for inserting an API key into the example app). TODO remove this once we start building the example app
4+
class MockRealtime: NSObject, ARTRealtimeProtocol {
5+
var device: ARTLocalDevice {
6+
fatalError("Not implemented")
7+
}
8+
9+
var clientId: String?
10+
11+
required init(options _: ARTClientOptions) {}
12+
13+
required init(key _: String) {}
14+
15+
required init(token _: String) {}
16+
17+
func time(_: @escaping ARTDateTimeCallback) {
18+
fatalError("Not implemented")
19+
}
20+
21+
func ping(_: @escaping ARTCallback) {
22+
fatalError("Not implemented")
23+
}
24+
25+
func stats(_: @escaping ARTPaginatedStatsCallback) -> Bool {
26+
fatalError("Not implemented")
27+
}
28+
29+
func stats(_: ARTStatsQuery?, callback _: @escaping ARTPaginatedStatsCallback) throws {
30+
fatalError("Not implemented")
31+
}
32+
33+
func connect() {
34+
fatalError("Not implemented")
35+
}
36+
37+
func close() {
38+
fatalError("Not implemented")
39+
}
40+
}

Sources/AblyChat/.swiftlint.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
opt_in_rules:
2+
# Opt-in rules of type "idiomatic" that we’ve decided we want:
3+
- explicit_acl

Sources/AblyChat/AblyChat.swift

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// Describes what to do with realtime events that come in faster than the consumer of an `AsyncSequence` can handle them.
2+
/// (This is the same as `AsyncStream<T>.Continuation.BufferingPolicy` but with the generic type parameter `T` removed.)
3+
public enum BufferingPolicy {
4+
case unbounded
5+
case bufferingOldest(Int)
6+
case bufferingNewest(Int)
7+
}

Sources/AblyChat/ChatClient.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import Ably
2+
3+
public protocol ChatClient: AnyObject, Sendable {
4+
var rooms: any Rooms { get }
5+
var connection: any Connection { get }
6+
var clientID: String { get }
7+
var realtime: any ARTRealtimeProtocol { get }
8+
var clientOptions: ClientOptions { get }
9+
}
10+
11+
public final class DefaultChatClient: ChatClient {
12+
public init(realtime _: ARTRealtimeProtocol, clientOptions _: ClientOptions?) {
13+
// This one doesn’t do `fatalError`, so that I can call it in the example app
14+
}
15+
16+
public var rooms: any Rooms {
17+
fatalError("Not yet implemented")
18+
}
19+
20+
public var connection: any Connection {
21+
fatalError("Not yet implemented")
22+
}
23+
24+
public var clientID: String {
25+
fatalError("Not yet implemented")
26+
}
27+
28+
public var realtime: any ARTRealtimeProtocol {
29+
fatalError("Not yet implemented")
30+
}
31+
32+
public var clientOptions: ClientOptions {
33+
fatalError("Not yet implemented")
34+
}
35+
}
36+
37+
public struct ClientOptions: Sendable {
38+
public var logHandler: LogHandler?
39+
public var logLevel: LogLevel?
40+
41+
public init(logHandler: (any LogHandler)? = nil, logLevel: LogLevel? = nil) {
42+
self.logHandler = logHandler
43+
self.logLevel = logLevel
44+
}
45+
}

Sources/AblyChat/Connection.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Ably
2+
3+
public protocol Connection: AnyObject, Sendable {
4+
var status: any ConnectionStatus { get }
5+
}
6+
7+
public protocol ConnectionStatus: AnyObject, Sendable {
8+
var current: ConnectionLifecycle { get }
9+
var error: ARTErrorInfo? { get }
10+
func subscribe(bufferingPolicy: BufferingPolicy) -> Subscription<ConnectionStatusChange>
11+
}
12+
13+
public enum ConnectionLifecycle: Sendable {
14+
case initialized
15+
case connecting
16+
case connected
17+
case disconnected
18+
case suspended
19+
case failed
20+
}
21+
22+
public struct ConnectionStatusChange: Sendable {
23+
public var current: ConnectionLifecycle
24+
public var previous: ConnectionLifecycle
25+
public var error: ARTErrorInfo?
26+
public var retryIn: TimeInterval
27+
28+
public init(current: ConnectionLifecycle, previous: ConnectionLifecycle, error: ARTErrorInfo? = nil, retryIn: TimeInterval) {
29+
self.current = current
30+
self.previous = previous
31+
self.error = error
32+
self.retryIn = retryIn
33+
}
34+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Ably
2+
3+
public protocol EmitsDiscontinuities {
4+
func subscribeToDiscontinuities() -> Subscription<ARTErrorInfo>
5+
}

Sources/AblyChat/Headers.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public typealias Headers = Any & Sendable /* TODO: Record<string, number | string | boolean | null | undefined>; */

Sources/AblyChat/Logging.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
public typealias LogContext = [String: Any]
2+
3+
public protocol LogHandler: AnyObject, Sendable {
4+
func log(message: String, level: LogLevel, context: LogContext?)
5+
}
6+
7+
public enum LogLevel: Sendable {
8+
case trace
9+
case debug
10+
case info
11+
case warn
12+
case error
13+
case silent
14+
}

Sources/AblyChat/Message.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import Foundation
2+
3+
public typealias MessageHeaders = Headers
4+
public typealias MessageMetadata = Metadata
5+
6+
public struct Message: Sendable {
7+
public var timeserial: String
8+
public var clientID: String
9+
public var roomID: String
10+
public var text: String
11+
public var createdAt: Date
12+
public var metadata: MessageMetadata
13+
public var headers: MessageHeaders
14+
15+
public init(timeserial: String, clientID: String, roomID: String, text: String, createdAt: Date, metadata: any MessageMetadata, headers: any MessageHeaders) {
16+
self.timeserial = timeserial
17+
self.clientID = clientID
18+
self.roomID = roomID
19+
self.text = text
20+
self.createdAt = createdAt
21+
self.metadata = metadata
22+
self.headers = headers
23+
}
24+
25+
public func isBefore(_: Message) -> Bool {
26+
fatalError("Not yet implemented")
27+
}
28+
29+
public func isAfter(_: Message) -> Bool {
30+
fatalError("Not yet implemented")
31+
}
32+
33+
public func isEqual(_: Message) -> Bool {
34+
fatalError("Not yet implemented")
35+
}
36+
}

Sources/AblyChat/Messages.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import Ably
2+
3+
public protocol Messages: AnyObject, Sendable, EmitsDiscontinuities {
4+
func subscribe(bufferingPolicy: BufferingPolicy) -> MessageSubscription
5+
func get(options: QueryOptions) async throws -> any PaginatedResult<Message>
6+
func send(params: SendMessageParams) async throws -> Message
7+
var channel: ARTRealtimeChannelProtocol { get }
8+
}
9+
10+
public struct SendMessageParams: Sendable {
11+
public var text: String
12+
public var metadata: MessageMetadata?
13+
public var headers: MessageHeaders?
14+
15+
public init(text: String, metadata: (any MessageMetadata)? = nil, headers: (any MessageHeaders)? = nil) {
16+
self.text = text
17+
self.metadata = metadata
18+
self.headers = headers
19+
}
20+
}
21+
22+
public struct QueryOptions: Sendable {
23+
public enum Direction: Sendable {
24+
case forwards
25+
case backwards
26+
}
27+
28+
public var start: Date?
29+
public var end: Date?
30+
public var limit: Int?
31+
public var direction: Direction?
32+
33+
public init(start: Date? = nil, end: Date? = nil, limit: Int? = nil, direction: QueryOptions.Direction? = nil) {
34+
self.start = start
35+
self.end = end
36+
self.limit = limit
37+
self.direction = direction
38+
}
39+
}
40+
41+
public struct QueryOptionsWithoutDirection: Sendable {
42+
public var start: Date?
43+
public var end: Date?
44+
public var limit: Int?
45+
46+
public init(start: Date? = nil, end: Date? = nil, limit: Int? = nil) {
47+
self.start = start
48+
self.end = end
49+
self.limit = limit
50+
}
51+
}
52+
53+
// TODO: note this will start accumulating messages as soon as created
54+
// TODO: note that I wanted this to instead inherit from Sequence protocol but that's not possible
55+
public struct MessageSubscription: Sendable, AsyncSequence {
56+
public typealias Element = Message
57+
58+
// TODO: explain, this is a workaround to allow us to write mocks
59+
public init<T: AsyncSequence>(mockAsyncSequence _: T) where T.Element == Element {
60+
fatalError("Not yet implemented")
61+
}
62+
63+
public func getPreviousMessages(params _: QueryOptionsWithoutDirection) async throws -> any PaginatedResult<Message> {
64+
fatalError("Not yet implemented")
65+
}
66+
67+
public struct AsyncIterator: AsyncIteratorProtocol {
68+
// note that I’ve removed the `throws` here and that means we don't need a `try` in the loop
69+
public mutating func next() async -> Element? {
70+
fatalError("Not implemented")
71+
}
72+
}
73+
74+
public func makeAsyncIterator() -> AsyncIterator {
75+
fatalError("Not implemented")
76+
}
77+
}

Sources/AblyChat/Metadata.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
public typealias Metadata = Any & Sendable // TODO: Record<string, unknown>;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
public protocol PaginatedResult<T>: AnyObject, Sendable {
2+
associatedtype T
3+
4+
var items: [T] { get }
5+
var hasNext: Bool { get }
6+
var isLast: Bool { get }
7+
// TODO: is there a way to link `hasNext` and `next`’s nullability?
8+
var next: (any PaginatedResult<T>)? { get async throws }
9+
var first: any PaginatedResult<T> { get async throws }
10+
var current: Bool { get async throws }
11+
}

0 commit comments

Comments
 (0)