Skip to content

Commit

Permalink
wip public API
Browse files Browse the repository at this point in the history
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()`)
  • Loading branch information
lawrence-forooghian committed Aug 14, 2024
1 parent 8c7b0a4 commit 4c2c98a
Show file tree
Hide file tree
Showing 28 changed files with 580 additions and 10 deletions.
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ opt_in_rules:
# Opt-in rules of type "lint" that we’ve decided we want:
- array_init
- empty_xctest_method
- missing_docs
- override_in_extension
- yoda_condition
- private_swiftui_state
Expand Down
12 changes: 12 additions & 0 deletions Example/AblyChatExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

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

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

/* Begin PBXGroup section */
212F95A52C6CAD7E00420287 /* Mocks */ = {
isa = PBXGroup;
children = (
212F95A62C6CAD9300420287 /* MockRealtime.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
21971DFD2C60D89C0074B8AE /* Frameworks */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -62,6 +72,7 @@
21F09A9E2C60CAF00025AF73 /* AblyChatExample */ = {
isa = PBXGroup;
children = (
212F95A52C6CAD7E00420287 /* Mocks */,
21F09A9F2C60CAF00025AF73 /* AblyChatExampleApp.swift */,
21F09AA12C60CAF00025AF73 /* ContentView.swift */,
21F09AA32C60CAF20025AF73 /* Assets.xcassets */,
Expand Down Expand Up @@ -152,6 +163,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
212F95A72C6CAD9300420287 /* MockRealtime.swift in Sources */,
21F09AA22C60CAF00025AF73 /* ContentView.swift in Sources */,
21F09AA02C60CAF00025AF73 /* AblyChatExampleApp.swift in Sources */,
);
Expand Down
5 changes: 4 additions & 1 deletion Example/AblyChatExample/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import SwiftUI

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

var body: some View {
VStack {
Expand Down
40 changes: 40 additions & 0 deletions Example/AblyChatExample/Mocks/MockRealtime.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Ably

/// 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
class MockRealtime: NSObject, ARTRealtimeProtocol {
var device: ARTLocalDevice {
fatalError("Not implemented")
}

var clientId: String?

required init(options _: ARTClientOptions) {}

required init(key _: String) {}

required init(token _: String) {}

func time(_: @escaping ARTDateTimeCallback) {
fatalError("Not implemented")
}

func ping(_: @escaping ARTCallback) {
fatalError("Not implemented")
}

func stats(_: @escaping ARTPaginatedStatsCallback) -> Bool {
fatalError("Not implemented")
}

func stats(_: ARTStatsQuery?, callback _: @escaping ARTPaginatedStatsCallback) throws {
fatalError("Not implemented")
}

func connect() {
fatalError("Not implemented")
}

func close() {
fatalError("Not implemented")
}
}
3 changes: 3 additions & 0 deletions Sources/AblyChat/.swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
opt_in_rules:
# Opt-in rules of type "idiomatic" that we’ve decided we want:
- explicit_acl
7 changes: 0 additions & 7 deletions Sources/AblyChat/AblyChat.swift

This file was deleted.

7 changes: 7 additions & 0 deletions Sources/AblyChat/BufferingPolicy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/// Describes what to do with realtime events that come in faster than the consumer of an `AsyncSequence` can handle them.
/// (This is the same as `AsyncStream<T>.Continuation.BufferingPolicy` but with the generic type parameter `T` removed.)
public enum BufferingPolicy {
case unbounded
case bufferingOldest(Int)
case bufferingNewest(Int)
}
45 changes: 45 additions & 0 deletions Sources/AblyChat/ChatClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Ably

public protocol ChatClient: AnyObject, Sendable {
var rooms: any Rooms { get }
var connection: any Connection { get }
var clientID: String { get }
var realtime: any ARTRealtimeProtocol { get }
var clientOptions: ClientOptions { get }
}

public final class DefaultChatClient: ChatClient {
public init(realtime _: ARTRealtimeProtocol, clientOptions _: ClientOptions?) {
// This one doesn’t do `fatalError`, so that I can call it in the example app
}

public var rooms: any Rooms {
fatalError("Not yet implemented")
}

public var connection: any Connection {
fatalError("Not yet implemented")
}

public var clientID: String {
fatalError("Not yet implemented")
}

public var realtime: any ARTRealtimeProtocol {
fatalError("Not yet implemented")
}

public var clientOptions: ClientOptions {
fatalError("Not yet implemented")
}
}

public struct ClientOptions: Sendable {
public var logHandler: LogHandler?
public var logLevel: LogLevel?

public init(logHandler: (any LogHandler)? = nil, logLevel: LogLevel? = nil) {
self.logHandler = logHandler
self.logLevel = logLevel
}
}
34 changes: 34 additions & 0 deletions Sources/AblyChat/Connection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Ably

public protocol Connection: AnyObject, Sendable {
var status: any ConnectionStatus { get }
}

public protocol ConnectionStatus: AnyObject, Sendable {
var current: ConnectionLifecycle { get }
var error: ARTErrorInfo? { get }
func subscribe(bufferingPolicy: BufferingPolicy) -> Subscription<ConnectionStatusChange>
}

public enum ConnectionLifecycle: Sendable {
case initialized
case connecting
case connected
case disconnected
case suspended
case failed
}

public struct ConnectionStatusChange: Sendable {
public var current: ConnectionLifecycle
public var previous: ConnectionLifecycle
public var error: ARTErrorInfo?
public var retryIn: TimeInterval

public init(current: ConnectionLifecycle, previous: ConnectionLifecycle, error: ARTErrorInfo? = nil, retryIn: TimeInterval) {
self.current = current
self.previous = previous
self.error = error
self.retryIn = retryIn
}
}
5 changes: 5 additions & 0 deletions Sources/AblyChat/EmitsDiscontinuities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Ably

public protocol EmitsDiscontinuities {
func subscribeToDiscontinuities() -> Subscription<ARTErrorInfo>
}
1 change: 1 addition & 0 deletions Sources/AblyChat/Headers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public typealias Headers = Any & Sendable /* TODO: Record<string, number | string | boolean | null | undefined>; */
14 changes: 14 additions & 0 deletions Sources/AblyChat/Logging.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
public typealias LogContext = [String: Any]

public protocol LogHandler: AnyObject, Sendable {
func log(message: String, level: LogLevel, context: LogContext?)
}

public enum LogLevel: Sendable {
case trace
case debug
case info
case warn
case error
case silent
}
36 changes: 36 additions & 0 deletions Sources/AblyChat/Message.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation

public typealias MessageHeaders = Headers
public typealias MessageMetadata = Metadata

public struct Message: Sendable {
public var timeserial: String
public var clientID: String
public var roomID: String
public var text: String
public var createdAt: Date
public var metadata: MessageMetadata
public var headers: MessageHeaders

public init(timeserial: String, clientID: String, roomID: String, text: String, createdAt: Date, metadata: any MessageMetadata, headers: any MessageHeaders) {
self.timeserial = timeserial
self.clientID = clientID
self.roomID = roomID
self.text = text
self.createdAt = createdAt
self.metadata = metadata
self.headers = headers
}

public func isBefore(_: Message) -> Bool {
fatalError("Not yet implemented")
}

public func isAfter(_: Message) -> Bool {
fatalError("Not yet implemented")
}

public func isEqual(_: Message) -> Bool {
fatalError("Not yet implemented")
}
}
77 changes: 77 additions & 0 deletions Sources/AblyChat/Messages.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Ably

public protocol Messages: AnyObject, Sendable, EmitsDiscontinuities {
func subscribe(bufferingPolicy: BufferingPolicy) -> MessageSubscription
func get(options: QueryOptions) async throws -> any PaginatedResult<Message>
func send(params: SendMessageParams) async throws -> Message
var channel: ARTRealtimeChannelProtocol { get }
}

public struct SendMessageParams: Sendable {
public var text: String
public var metadata: MessageMetadata?
public var headers: MessageHeaders?

public init(text: String, metadata: (any MessageMetadata)? = nil, headers: (any MessageHeaders)? = nil) {
self.text = text
self.metadata = metadata
self.headers = headers
}
}

public struct QueryOptions: Sendable {
public enum Direction: Sendable {
case forwards
case backwards
}

public var start: Date?
public var end: Date?
public var limit: Int?
public var direction: Direction?

public init(start: Date? = nil, end: Date? = nil, limit: Int? = nil, direction: QueryOptions.Direction? = nil) {
self.start = start
self.end = end
self.limit = limit
self.direction = direction
}
}

public struct QueryOptionsWithoutDirection: Sendable {
public var start: Date?
public var end: Date?
public var limit: Int?

public init(start: Date? = nil, end: Date? = nil, limit: Int? = nil) {
self.start = start
self.end = end
self.limit = limit
}
}

// TODO: note this will start accumulating messages as soon as created
// TODO: note that I wanted this to instead inherit from Sequence protocol but that's not possible
public struct MessageSubscription: Sendable, AsyncSequence {
public typealias Element = Message

// TODO: explain, this is a workaround to allow us to write mocks
public init<T: AsyncSequence>(mockAsyncSequence _: T) where T.Element == Element {
fatalError("Not yet implemented")
}

public func getPreviousMessages(params _: QueryOptionsWithoutDirection) async throws -> any PaginatedResult<Message> {
fatalError("Not yet implemented")
}

public struct AsyncIterator: AsyncIteratorProtocol {
// note that I’ve removed the `throws` here and that means we don't need a `try` in the loop
public mutating func next() async -> Element? {
fatalError("Not implemented")
}
}

public func makeAsyncIterator() -> AsyncIterator {
fatalError("Not implemented")
}
}
1 change: 1 addition & 0 deletions Sources/AblyChat/Metadata.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public typealias Metadata = Any & Sendable // TODO: Record<string, unknown>;
17 changes: 17 additions & 0 deletions Sources/AblyChat/Occupancy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Ably

public protocol Occupancy: AnyObject, Sendable, EmitsDiscontinuities {
func subscribe(bufferingPolicy: BufferingPolicy) -> Subscription<OccupancyEvent>
func get() async throws -> OccupancyEvent
var channel: ARTRealtimeChannelProtocol { get }
}

public struct OccupancyEvent {
public var connections: Int
public var presenceMembers: Int

public init(connections: Int, presenceMembers: Int) {
self.connections = connections
self.presenceMembers = presenceMembers
}
}
11 changes: 11 additions & 0 deletions Sources/AblyChat/PaginatedResult.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
public protocol PaginatedResult<T>: AnyObject, Sendable {
associatedtype T

var items: [T] { get }
var hasNext: Bool { get }
var isLast: Bool { get }
// TODO: is there a way to link `hasNext` and `next`’s nullability?
var next: (any PaginatedResult<T>)? { get async throws }
var first: any PaginatedResult<T> { get async throws }
var current: Bool { get async throws }
}
Loading

0 comments on commit 4c2c98a

Please sign in to comment.