Skip to content
Draft
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
24 changes: 6 additions & 18 deletions .github/workflows/build_and_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,13 @@ jobs:
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Build and Test

- name: Test on iOS simulator
run: |
xcodebuild test -scheme PowerSync-Package -destination "platform=iOS Simulator,name=iPhone 16"
xcodebuild test -scheme PowerSync-Package -destination "platform=macOS,arch=arm64,name=My Mac"
xcodebuild test -scheme PowerSync-Package -destination "platform=watchOS Simulator,arch=arm64,name=Apple Watch Ultra 2 (49mm)"

buildSwift6:
name: Build and test with Swift 6
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Set up XCode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Use Swift 6
- name: Test on macOS
run: |
sed -i '' 's|^// swift-tools-version:.*$|// swift-tools-version:6.1|' Package.swift
- name: Build and Test
xcodebuild test -scheme PowerSync-Package -destination "platform=macOS,arch=arm64,name=My Mac"
- name: Test on watchOS simulator
run: |
swift build -Xswiftc -strict-concurrency=complete
swift test -Xswiftc -strict-concurrency=complete
xcodebuild test -scheme PowerSync-Package -destination "platform=watchOS Simulator,arch=arm64,name=Apple Watch Ultra 2 (49mm)"
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Changelog

## 1.6.1 (unreleased)
## 1.7.0 (unreleased)

* Update Kotlin SDK to 1.7.0.
* Update Kotlin SDK to 1.8.0.
* Add experimental support for [sync streams](https://docs.powersync.com/usage/sync-streams).

## 1.6.0

Expand Down
2 changes: 1 addition & 1 deletion Demo/PowerSyncExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objectVersion = 60;
objects = {

/* Begin PBXBuildFile section */
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions Demo/PowerSyncExample/Components/TodoListView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AVFoundation
import IdentifiedCollections
import PowerSync
import SwiftUI
import SwiftUINavigation

Expand All @@ -11,6 +12,7 @@ struct TodoListView: View {
@State private var error: Error?
@State private var newTodo: NewTodo?
@State private var editing: Bool = false
@State private var loadingListItems: Bool = false

#if os(iOS)
// Called when a photo has been captured. Individual widgets should register the listener
Expand All @@ -33,6 +35,10 @@ struct TodoListView: View {
}
}
}

if (loadingListItems) {
ProgressView()
}

ForEach(todos) { todo in
#if os(iOS)
Expand Down Expand Up @@ -142,6 +148,22 @@ struct TodoListView: View {
}
}
}
.task {
if (Secrets.previewSyncStreams) {
// With sync streams, todo items are not loaded by default. We have to request them while we need them.
// Thanks to builtin caching, navingating to the same list multiple times does not have to fetch items again.
loadingListItems = true
do {
// This will make the sync client request items from this list as long as we keep a reference to the stream subscription,
// and a default TTL of one day afterwards.
let streamSubscription = try await system.db.syncStream(name: "todos", params: ["list": JsonValue.string(listId)]).subscribe()
try await streamSubscription.waitForFirstSync()
} catch {
print("Error subscribing to list stream \(error)")
}
loadingListItems = false
}
}
}

func toggleCompletion(of todo: Todo) async {
Expand Down
5 changes: 3 additions & 2 deletions Demo/PowerSyncExample/PowerSync/SystemManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,12 @@ final class SystemManager {
options: ConnectOptions(
clientConfiguration: SyncClientConfiguration(
requestLogger: SyncRequestLoggerConfiguration(
requestLevel: .headers
requestLevel: .all
) { message in
self.db.logger.debug(message, tag: "SyncRequest")
}
)
),
newClientImplementation: true,
)
)
try await attachments?.startSync()
Expand Down
1 change: 0 additions & 1 deletion Demo/PowerSyncExample/Screens/HomeScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ struct HomeScreen: View {


var body: some View {

ListView()
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Expand Down
20 changes: 20 additions & 0 deletions Demo/PowerSyncExample/Secrets.template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,24 @@ extension Secrets {
static var supabaseStorageBucket: String? {
return nil
}

static var previewSyncStreams: Bool {
/*
Set to true to preview https://docs.powersync.com/usage/sync-streams.
When enabling this, also set your sync rules to the following:

streams:
lists:
query: SELECT * FROM lists WHERE owner_id = auth.user_id()
auto_subscribe: true
todos:
query: SELECT * FROM todos WHERE list_id = subscription.parameter('list') AND list_id IN (SELECT id FROM lists WHERE owner_id = auth.user_id())

config:
edition: 2

*/

false
}
}
1 change: 1 addition & 0 deletions Demo/PowerSyncExample/_Secrets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ protocol SecretsProvider {
static var supabaseURL: URL { get }
static var supabaseAnonKey: String { get }
static var supabaseStorageBucket: String? { get }
static var previewSyncStreams: Bool { get }
}

// Default conforming type
Expand Down
16 changes: 13 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 30 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.7
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -7,7 +7,7 @@ let packageName = "PowerSync"

// Set this to the absolute path of your Kotlin SDK checkout if you want to use a local Kotlin
// build. Also see docs/LocalBuild.md for details
let localKotlinSdkOverride: String? = nil
let localKotlinSdkOverride: String? = "/Users/simon/src/powersync-kotlin"

// Set this to the absolute path of your powersync-sqlite-core checkout if you want to use a
// local build of the core extension.
Expand All @@ -18,22 +18,43 @@ let localCoreExtension: String? = nil
// a binary target.
// With a local SDK, we point to a `Package.swift` within the Kotlin SDK containing a target pointing
// towards a local framework build
var conditionalDependencies: [Package.Dependency] = []
var conditionalDependencies: [Package.Dependency] = [
.package(
url: "https://github.com/sbooth/CSQLite.git",
from: "3.50.4",
traits: [
.defaults,
// CSQLite uses THREADSAFE=0 by default, which breaks PowerSync because we're using SQLite on
// multiple threads (it can lead to race conditions when closing connections sharing resources
// like shared memory, causing crashes).
// THREADSAFE=2 overrides the default, and is safe to use as long as a single SQLite connection
// is not shared between threads.
// TODO: Technically, we should not use .defaults because there's a logical conflict between
// the threadsafe options. Instead, we should spell out all defaults again and remove that
// thread-safety option.
// However, despite the docs explicitly saying something else, it looks like there's no way to
// disable default traits anyway (XCode compiles sqlite3.c with the default option even without
// .defaults being included here).
"THREADSAFE_2",
"ENABLE_SESSION"
]
)
]
var conditionalTargets: [Target] = []
var kotlinTargetDependency = Target.Dependency.target(name: "PowerSyncKotlin")

if let kotlinSdkPath = localKotlinSdkOverride {
// We can't depend on local XCFrameworks outside of this project's root, so there's a Package.swift
// in the PowerSyncKotlin project pointing towards a local build.
conditionalDependencies.append(.package(path: "\(kotlinSdkPath)/PowerSyncKotlin"))
conditionalDependencies.append(.package(path: "\(kotlinSdkPath)/internal/PowerSyncKotlin"))

kotlinTargetDependency = .product(name: "PowerSyncKotlin", package: "PowerSyncKotlin")
} else {
// Not using a local build, so download from releases
conditionalTargets.append(.binaryTarget(
name: "PowerSyncKotlin",
url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.7.0/PowersyncKotlinRelease.zip",
checksum: "836ac106c26a184c10373c862745d9af195737ad01505bb965f197797aa88535"
url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.8.0/PowersyncKotlinRelease.zip",
checksum: "31ac7c5e11d747e11bceb0b34f30438d37033e700c621b0a468aa308d887587f"
))
}

Expand All @@ -45,7 +66,7 @@ if let corePath = localCoreExtension {
// Not using a local build, so download from releases
conditionalDependencies.append(.package(
url: "https://github.com/powersync-ja/powersync-sqlite-core-swift.git",
exact: "0.4.6"
exact: "0.4.8"
))
}

Expand Down Expand Up @@ -81,7 +102,8 @@ let package = Package(
name: packageName,
dependencies: [
kotlinTargetDependency,
.product(name: "PowerSyncSQLiteCore", package: corePackageName)
.product(name: "PowerSyncSQLiteCore", package: corePackageName),
.product(name: "CSQLite", package: "CSQLite")
]
),
.testTarget(
Expand Down
3 changes: 2 additions & 1 deletion Sources/PowerSync/Kotlin/KotlinAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ enum KotlinAdapter {
return PowerSyncKotlin.RawTable(
name: table.name,
put: translateStatement(table.put),
delete: translateStatement(table.delete)
delete: translateStatement(table.delete),
clear: table.clear,
);
}

Expand Down
18 changes: 15 additions & 3 deletions Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import PowerSyncKotlin
import CSQLite

final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
// `PowerSyncKotlin.PowerSyncDatabase` cannot be marked as Sendable
Expand All @@ -15,7 +16,12 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
dbFilename: String,
logger: DatabaseLogger
) {
let factory = PowerSyncKotlin.DatabaseDriverFactory()
let rc = sqlite3_initialize();
if (rc != 0) {
fatalError("Call to sqlite3_initialize() failed with \(rc)")
}

let factory = sqlite3DatabaseFactory(initialStatements: [])
kotlinDatabase = PowerSyncDatabase(
factory: factory,
schema: KotlinAdapter.Schema.toKotlin(schema),
Expand Down Expand Up @@ -87,9 +93,10 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
try await kotlinDatabase.disconnect()
}

func disconnectAndClear(clearLocal: Bool = true) async throws {
func disconnectAndClear(clearLocal: Bool, soft: Bool) async throws {
try await kotlinDatabase.disconnectAndClear(
clearLocal: clearLocal
clearLocal: clearLocal,
soft: soft
)
}

Expand Down Expand Up @@ -322,6 +329,11 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol,
func close() async throws {
try await kotlinDatabase.close()
}

func syncStream(name: String, params: JsonParam?) -> any SyncStream {
let rawStream = kotlinDatabase.syncStream(name: name, parameters: params?.mapValues { $0.toKotlinMap() });
return KotlinSyncStream(kotlinStream: rawStream)
}

/// Tries to convert Kotlin PowerSyncExceptions to Swift Exceptions
private func wrapPowerSyncException<R: Sendable>(
Expand Down
18 changes: 18 additions & 0 deletions Sources/PowerSync/Kotlin/sync/KotlinSyncStatusData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ extension KotlinSyncStatusDataProtocol {
)
)
}

var syncStreams: [SyncStreamStatus]? {
return base.syncStreams?.map(mapSyncStreamStatus)
}

func forStream(stream: SyncStreamDescription) -> SyncStreamStatus? {
var rawStatus: Optional<PowerSyncKotlin.SyncStreamStatus>
if let kotlinStream = stream as? any HasKotlinStreamDescription {
// Fast path: Reuse Kotlin stream object for lookup.
rawStatus = base.forStream(stream: kotlinStream.kotlinDescription)
} else {
// Custom stream description, we have to convert parameters to a Kotlin map.
let parameters = stream.parameters?.mapValues { $0.toValue() }
rawStatus = syncStatusForStream(status: base, name: stream.name, parameters: parameters)
}

return rawStatus.map(mapSyncStreamStatus)
}

private func mapPriorityStatus(_ status: PowerSyncKotlin.PriorityStatusEntry) -> PriorityStatusEntry {
var lastSyncedAt: Date?
Expand Down
Loading
Loading