Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: realtime support #2

Merged
merged 4 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/nicklockwood/SwiftFormat",
"state" : {
"revision" : "d37a477177d5d4ff2a3ae6328eaaab5bf793e702",
"version" : "0.52.9"
"revision" : "cac06079ce883170ab44cb021faad298daeec2a5",
"version" : "0.52.10"
}
}
],
Expand Down
20 changes: 14 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import PackageDescription
let package = Package(
name: "FalClient",
platforms: [
.iOS(.v13),
.macOS(.v11),
.iOS(.v15),
.macOS(.v12),
.macCatalyst(.v13),
.tvOS(.v13),
.watchOS(.v8),
Expand All @@ -20,17 +20,25 @@ let package = Package(
),
],
dependencies: [
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.4"),
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.52.10"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "FalClient"),
.target(name: "FalClient"),
.testTarget(
name: "FalClientTests",
dependencies: ["FalClient"]
),
.target(name: "FalSampleApp", dependencies: ["FalClient"]),
.target(
name: "FalSampleApp",
dependencies: ["FalClient"],
path: "Sources/Samples/FalSampleApp"
),
.target(
name: "FalRealtimeSampleApp",
dependencies: ["FalClient"],
path: "Sources/Samples/FalRealtimeSampleApp"
),
]
)
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ This Swift client library is crafted as a lightweight layer atop Swift's network
2. Set up the client instance:
```swift
import FalClient
let fal = FalClient(withCredentials: "FAL_KEY_ID:FAL_KEY_SECRET")
let fal = FalClient.withCredentials(.keyPair("FAL_KEY_ID:FAL_KEY_SECRET"))

// You can also use a proxy to protect your credentials
// let fal = FalClient.withProxy("http://localhost:3333/api/fal/proxy")
```

3. Use `fal.subscribe` to dispatch requests to the model API:
Expand All @@ -40,11 +43,49 @@ This Swift client library is crafted as a lightweight layer atop Swift's network
```

**Notes:**

- Replace `text-to-image` with a valid model id. Check [fal.ai/models](https://fal.ai/models) for all available models.
- It fully relies on `async/await` for asynchronous programming.
- The result type in Swift will be a `[String: Any]` and the entries depend on the API output schema.
- The Swift client also supports typed inputs and outputs through `Codable`.

## Real-time

The client supports real-time model APIs. Checkout the [FalRealtimeSampleApp](./Sources/Samples/FalRealtimeSampleApp/) for more details.

```swift
let connection = try fal.realtime.connect(
to: OptimizedLatentConsistency,
connectionKey: "PencilKitDemo",
throttleInterval: .milliseconds(128)
) { (result: Result<LcmResponse, Error>) in
if case let .success(data) = result,
let image = data.images.first {
let data = try? Data(contentsOf: URL(string: image.url)!)
DispatchQueue.main.async {
self.currentImage = data
}
}
}

try connection.send(LcmInput(
prompt: prompt,
imageUrl: "data:image/jpeg;base64,\(drawing.base64EncodedString())",
seed: 6_252_023,
syncMode: true
))
```

## Sample apps

Check the `Sources/Samples` folder for a handful of sample applications using the `FalClient`.

Open them with `xed` to quickly start playing with

```bash
xed Sources/Sample/FalSampleApp
```

## Roadmap

See the [open feature requests](https://github.com/fal-ai/serverless-client-swift/labels/enhancement) for a list of proposed features and join the discussion.
Expand Down
19 changes: 10 additions & 9 deletions Sources/FalClient/Client+Codable.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Dispatch
import Foundation

public struct EmptyInput: Encodable {
Expand All @@ -18,7 +19,7 @@ public extension Client {
}

func run<Output: Decodable>(
_ id: String,
_ app: String,
input: (some Encodable) = EmptyInput.empty,
options: RunOptions = DefaultRunOptions
) async throws -> Output {
Expand All @@ -27,25 +28,25 @@ public extension Client {
? try JSONSerialization.jsonObject(with: inputData!) as? [String: Any]
: nil

let data = try await sendRequest(id, input: inputData, queryParams: queryParams, options: options)
let url = buildUrl(fromId: app, path: options.path)
let data = try await sendRequest(url, input: inputData, queryParams: queryParams, options: options)
return try decoder.decode(Output.self, from: data)
}

func subscribe<Output: Decodable>(
_ id: String,
to app: String,
input: (some Encodable) = EmptyInput.empty,
pollInterval: FalTimeInterval = .seconds(1),
timeout: FalTimeInterval = .minutes(3),
pollInterval: DispatchTimeInterval = .seconds(1),
timeout: DispatchTimeInterval = .minutes(3),
includeLogs: Bool = false,
options _: RunOptions = DefaultRunOptions,
onQueueUpdate: OnQueueUpdate? = nil
) async throws -> Output {
let requestId = try await queue.submit(id, input: input)
let requestId = try await queue.submit(app, input: input)
let start = Int(Date().timeIntervalSince1970 * 1000)
var elapsed = 0
var isCompleted = false
while elapsed < timeout.milliseconds {
let update = try await queue.status(id, of: requestId, includeLogs: includeLogs)
let update = try await queue.status(app, of: requestId, includeLogs: includeLogs)
if let onQueueUpdateCallback = onQueueUpdate {
onQueueUpdateCallback(update)
}
Expand All @@ -59,6 +60,6 @@ public extension Client {
if !isCompleted {
throw FalError.queueTimeout
}
return try await queue.response(id, of: requestId)
return try await queue.response(app, of: requestId)
}
}
5 changes: 2 additions & 3 deletions Sources/FalClient/Client+Request.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Foundation

extension Client {
func sendRequest(_ id: String, input: Data?, queryParams: [String: Any]? = nil, options: RequestOptions) async throws -> Data {
let urlString = buildUrl(fromId: id, path: options.path)
func sendRequest(_ urlString: String, input: Data?, queryParams: [String: Any]? = nil, options: RunOptions) async throws -> Data {
guard var url = URL(string: urlString) else {
throw FalError.invalidUrl(url: urlString)
}
Expand Down Expand Up @@ -49,6 +48,6 @@ extension Client {

var userAgent: String {
let osVersion = ProcessInfo.processInfo.operatingSystemVersionString
return "fal.ai/swift-client 0.0.1 - \(osVersion)"
return "fal.ai/swift-client 0.1.0 - \(osVersion)"
}
}
36 changes: 18 additions & 18 deletions Sources/FalClient/Client.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import Dispatch
import Foundation

enum HttpMethod: String {
public enum HttpMethod: String {
case get
case post
case put
case delete
}

protocol RequestOptions {
public protocol RequestOptions {
var httpMethod: HttpMethod { get }
var path: String { get }
}

public struct RunOptions: RequestOptions {
let path: String
let httpMethod: HttpMethod
public let path: String
public let httpMethod: HttpMethod

static func withMethod(_ method: HttpMethod) -> Self {
static func withMethod(_ method: HttpMethod) -> RunOptions {
RunOptions(path: "", httpMethod: method)
}

static func route(_ path: String, withMethod method: HttpMethod = .post) -> Self {
static func route(_ path: String, withMethod method: HttpMethod = .post) -> RunOptions {
RunOptions(path: path, httpMethod: method)
}
}
Expand All @@ -34,39 +35,38 @@ public protocol Client {

var queue: Queue { get }

var realtime: Realtime { get }

func run(_ id: String, input: [String: Any]?, options: RunOptions) async throws -> [String: Any]

func subscribe(
_ id: String,
to app: String,
input: [String: Any]?,
pollInterval: FalTimeInterval,
timeout: FalTimeInterval,
pollInterval: DispatchTimeInterval,
timeout: DispatchTimeInterval,
includeLogs: Bool,
options: RunOptions,
onQueueUpdate: OnQueueUpdate?
) async throws -> [String: Any]
}

public extension Client {
func run(_ id: String, input: [String: Any]? = nil, options: RunOptions = DefaultRunOptions) async throws -> [String: Any] {
return try await run(id, input: input, options: options)
func run(_ app: String, input: [String: Any]? = nil, options: RunOptions = DefaultRunOptions) async throws -> [String: Any] {
return try await run(app, input: input, options: options)
}

func subscribe(
_ id: String,
to app: String,
input: [String: Any]? = nil,
pollInterval: FalTimeInterval = .seconds(1),
timeout: FalTimeInterval = .minutes(3),
pollInterval: DispatchTimeInterval = .seconds(1),
timeout: DispatchTimeInterval = .minutes(3),
includeLogs: Bool = false,
options: RunOptions = DefaultRunOptions,
onQueueUpdate: OnQueueUpdate? = nil
) async throws -> [String: Any] {
return try await subscribe(id,
return try await subscribe(to: app,
input: input,
pollInterval: pollInterval,
timeout: timeout,
includeLogs: includeLogs,
options: options,
onQueueUpdate: onQueueUpdate)
}
}
27 changes: 17 additions & 10 deletions Sources/FalClient/FalClient.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Dispatch
import Foundation

func buildUrl(fromId id: String, path: String? = nil) -> String {
Expand Down Expand Up @@ -28,31 +29,33 @@ public struct FalClient: Client {

public var queue: Queue { QueueClient(client: self) }

public func run(_ id: String, input: [String: Any]?, options: RunOptions) async throws -> [String: Any] {
public var realtime: Realtime { RealtimeClient(client: self) }

public func run(_ app: String, input: [String: Any]?, options: RunOptions) async throws -> [String: Any] {
let inputData = input != nil ? try JSONSerialization.data(withJSONObject: input as Any) : nil
let queryParams = options.httpMethod == .get ? input : nil
let data = try await sendRequest(id, input: inputData, queryParams: queryParams, options: options)
let url = buildUrl(fromId: app, path: options.path)
let data = try await sendRequest(url, input: inputData, queryParams: queryParams, options: options)
guard let result = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw FalError.invalidResultFormat
}
return result
}

public func subscribe(
_ id: String,
to app: String,
input: [String: Any]?,
pollInterval: FalTimeInterval,
timeout: FalTimeInterval,
pollInterval: DispatchTimeInterval,
timeout: DispatchTimeInterval,
includeLogs: Bool,
options _: RunOptions,
onQueueUpdate: OnQueueUpdate?
) async throws -> [String: Any] {
let requestId = try await queue.submit(id, input: input)
let requestId = try await queue.submit(app, input: input)
let start = Int(Date().timeIntervalSince1970 * 1000)
var elapsed = 0
var isCompleted = false
while elapsed < timeout.milliseconds {
let update = try await queue.status(id, of: requestId, includeLogs: includeLogs)
let update = try await queue.status(app, of: requestId, includeLogs: includeLogs)
if let onQueueUpdateCallback = onQueueUpdate {
onQueueUpdateCallback(update)
}
Expand All @@ -66,12 +69,16 @@ public struct FalClient: Client {
if !isCompleted {
throw FalError.queueTimeout
}
return try await queue.response(id, of: requestId)
return try await queue.response(app, of: requestId)
}
}

public extension FalClient {
static func withProxy(_ url: String) -> FalClient {
static func withProxy(_ url: String) -> Client {
return FalClient(config: ClientConfig(requestProxy: url))
}

static func withCredentials(_ credentials: ClientCredentials) -> Client {
return FalClient(config: ClientConfig(credentials: credentials))
}
}
19 changes: 0 additions & 19 deletions Sources/FalClient/FalTimeInterval.swift

This file was deleted.

30 changes: 30 additions & 0 deletions Sources/FalClient/Realtime+Codable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Dispatch
import Foundation

class CodableRealtimeConnection<Input: Encodable>: RealtimeConnection<Input> {
override public func send(_ data: Input) throws {
let json = try JSONEncoder().encode(data)
try sendReference(json)
}
}

public extension Realtime {
func connect<Input: Encodable, Output: Decodable>(
to app: String,
connectionKey: String,
throttleInterval: DispatchTimeInterval,
onResult completion: @escaping (Result<Output, Error>) -> Void
) throws -> RealtimeConnection<Input> {
return handleConnection(
to: app, connectionKey: connectionKey, throttleInterval: throttleInterval,
resultConverter: { data in
let result = try JSONDecoder().decode(Output.self, from: data)
return result
},
connectionFactory: { send, close in
CodableRealtimeConnection(send, close)
},
onResult: completion
)
}
}
Loading