diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
index 2e801bda..459ef788 100644
--- a/.github/workflows/build-docs.yml
+++ b/.github/workflows/build-docs.yml
@@ -25,8 +25,7 @@ jobs:
- name: Restore Projects
run: |
- dotnet restore PowerSync/PowerSync.Common/PowerSync.Common.csproj -p:TargetFrameworks=net8.0
- dotnet restore PowerSync/PowerSync.Maui/PowerSync.Maui.csproj -p:TargetFrameworks=net8.0
+ dotnet restore PowerSync/PowerSync.Common/PowerSync.Common.csproj -p:TargetFrameworks=net10.0
- name: Generate API Metadata
working-directory: docs
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 2a4db78b..162608c9 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -31,8 +31,7 @@ jobs:
- name: Restore Projects
run: |
- dotnet restore PowerSync/PowerSync.Common/PowerSync.Common.csproj -p:TargetFrameworks=net8.0
- dotnet restore PowerSync/PowerSync.Maui/PowerSync.Maui.csproj -p:TargetFrameworks=net8.0
+ dotnet restore PowerSync/PowerSync.Common/PowerSync.Common.csproj -p:TargetFrameworks=net10.0
- name: Generate API Metadata
working-directory: docs
diff --git a/.github/workflows/dev-packages.yml b/.github/workflows/dev-packages.yml
index e63e2fe6..dedef039 100644
--- a/.github/workflows/dev-packages.yml
+++ b/.github/workflows/dev-packages.yml
@@ -24,7 +24,7 @@ jobs:
with:
dotnet-version: "10.0.x"
- - name: Install MAUI Workloads
+ - name: Install native workloads
run: dotnet workload restore
- name: Download PowerSync extension
@@ -33,9 +33,6 @@ jobs:
- name: Restore dependencies
run: dotnet restore
- - name: Install Android SDK for net8.0-android
- run: dotnet build PowerSync/PowerSync.Common -t:InstallAndroidDependencies -f net8.0-android -p:AndroidSdkDirectory="$ANDROID_HOME" -p:AcceptAndroidSdkLicenses=True
-
- name: Extract Common Package Version from CHANGELOG.md
shell: bash
run: |
@@ -50,20 +47,6 @@ jobs:
- name: Run Pack For Common
run: dotnet pack PowerSync/PowerSync.Common -c Release -o ${{ github.workspace }}/output
- - name: Extract MAUI Package Version from CHANGELOG.md
- shell: bash
- run: |
- MAUI_VERSION=$(awk '/^## [0-9]+\.[0-9]+\.[0-9]+-dev(\.[0-9]+)?$/ {print $2; exit}' PowerSync/PowerSync.Maui/CHANGELOG.md)
- if [[ -z "$MAUI_VERSION" ]]; then
- echo "Error: Invalid dev version found in PowerSync.Maui/CHANGELOG.md. Expected format: x.x.x-dev.x"
- exit 1
- fi
- echo "Detected Version: $MAUI_VERSION"
- echo "VERSION=$MAUI_VERSION" >> $GITHUB_ENV
-
- - name: Run Pack For MAUI
- run: dotnet pack PowerSync/PowerSync.Maui -c Release -o ${{ github.workspace }}/output
-
- name: Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
@@ -90,7 +73,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "8.0.x"
+ dotnet-version: "10.0.x"
- name: Install Sign CLI tool
run: dotnet tool install --tool-path . --prerelease sign
@@ -142,7 +125,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "8.0.x"
+ dotnet-version: "10.0.x"
- name: Validate artifact signatures
run: dotnet nuget verify ${{ github.workspace }}/SignedArtifacts/**/*.nupkg
@@ -158,9 +141,3 @@ jobs:
dotnet nuget push "${{ github.workspace }}\SignedArtifacts\PowerSync.Common*.nupkg"
--api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
--source https://api.nuget.org/v3/index.json
-
- - name: Run Push For Maui
- run: >
- dotnet nuget push "${{ github.workspace }}\SignedArtifacts\PowerSync.Maui*.nupkg"
- --api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
- --source https://api.nuget.org/v3/index.json
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 71c0874a..dfd311e4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -15,7 +15,6 @@ jobs:
outputs:
common_version: ${{ steps.extract_version.outputs.common_version }}
- maui_version: ${{ steps.extract_maui_version.outputs.maui_version }}
steps:
- name: Checkout Repository
@@ -28,7 +27,7 @@ jobs:
with:
dotnet-version: "10.0.x"
- - name: Install MAUI Workloads
+ - name: Install native workloads
run: dotnet workload restore
- name: Download PowerSync extension
@@ -37,9 +36,6 @@ jobs:
- name: Restore dependencies
run: dotnet restore
- - name: Install Android SDK for net8.0-android
- run: dotnet build PowerSync/PowerSync.Common -t:InstallAndroidDependencies -f net8.0-android -p:AndroidSdkDirectory="$ANDROID_HOME" -p:AcceptAndroidSdkLicenses=True
-
- name: Extract Common Package Version from CHANGELOG.md
id: extract_version
shell: bash
@@ -56,22 +52,6 @@ jobs:
- name: Run Pack For Common
run: dotnet pack PowerSync/PowerSync.Common -c Release -o ${{ github.workspace }}/output
- - name: Extract MAUI Package Version from CHANGELOG.md
- id: extract_maui_version
- shell: bash
- run: |
- MAUI_VERSION=$(awk '/^## [0-9]+\.[0-9]+\.[0-9]+/ {print $2; exit}' PowerSync/PowerSync.Maui/CHANGELOG.md)
- if [[ -z "$MAUI_VERSION" ]]; then
- echo "Error: Invalid version found in PowerSync.Maui/CHANGELOG.md. Expected format: '## x.x.x'"
- exit 1
- fi
- echo "Detected Version: $MAUI_VERSION"
- echo "VERSION=$MAUI_VERSION" >> $GITHUB_ENV
- echo "maui_version=$MAUI_VERSION" >> $GITHUB_OUTPUT
-
- - name: Run Pack For MAUI
- run: dotnet pack PowerSync/PowerSync.Maui -c Release -o ${{ github.workspace }}/output
-
- name: Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
@@ -98,7 +78,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "8.0.x"
+ dotnet-version: "10.0.x"
- name: Install Sign CLI tool
run: dotnet tool install --tool-path . --prerelease sign
@@ -150,7 +130,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "8.0.x"
+ dotnet-version: "10.0.x"
- name: Validate artifact signatures
run: dotnet nuget verify ${{ github.workspace }}/SignedArtifacts/**/*.nupkg
@@ -181,24 +161,3 @@ jobs:
--target main \
--generate-notes \
SignedArtifacts/PowerSync.Common*.nupkg
-
- - name: Run Push For Maui
- run: >
- dotnet nuget push "${{ github.workspace }}\SignedArtifacts\PowerSync.Maui*.nupkg"
- --api-key "${{ steps.login.outputs.NUGET_API_KEY }}"
- --source https://api.nuget.org/v3/index.json
-
- - name: Create GitHub Release For Maui
- continue-on-error: true
- shell: bash
- env:
- GH_TOKEN: ${{ github.token }}
- GH_REPO: ${{ github.repository }}
- MAUI_VERSION: ${{ needs.build.outputs.maui_version }}
- run: |
- gh release create "PowerSync.Maui@${MAUI_VERSION}" \
- --draft \
- --title "PowerSync.Maui@${MAUI_VERSION}" \
- --target main \
- --generate-notes \
- SignedArtifacts/PowerSync.Maui*.nupkg
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 110b65f1..a2920b91 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,7 +6,7 @@ on:
jobs:
build:
strategy:
- matrix:
+ matrix:
runner: [windows-latest, windows-11-arm]
name: Test Packages ${{ matrix.runner }}
runs-on: ${{ matrix.runner }}
@@ -18,16 +18,16 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
- dotnet-version: '10.0'
+ dotnet-version: "10.0"
- - name: Install MAUI Workloads
+ - name: Install native workloads
run: dotnet workload restore
-
+
- name: Download PowerSync extension
- run: dotnet run --project Tools/Setup
+ run: dotnet run --project Tools/Setup
- name: Restore dependencies
run: dotnet restore
-
+
- name: Run tests
run: dotnet test -v n --framework net10.0
diff --git a/.gitignore b/.gitignore
index 688d8e68..4f214e65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,18 +53,14 @@ TestResults/
*.VSIXProjectLaunch
.project
.vsconfig
+.env
*.db
*.db-shm
*.db-wal
-#Core binaries - add rules after ps extension downloading is supported
-*.dylib
-*.dll
-*.so
-*.xcframework
-.env
-*NativeLibs
+# PowerSync runtimes installed by Tools/Setup
+**/runtimes/
# Ignore user id file
user_id.txt
diff --git a/PowerSync/PowerSync.Common/CHANGELOG.md b/PowerSync/PowerSync.Common/CHANGELOG.md
index 04428fc4..204674da 100644
--- a/PowerSync/PowerSync.Common/CHANGELOG.md
+++ b/PowerSync/PowerSync.Common/CHANGELOG.md
@@ -4,7 +4,8 @@
- Full release.
- `PowerSyncDatabase.OnChange` now returns the underlying raw table name instead of the view name to mirror PowerSync JS.
-- Use `long` for op IDs instead of `string`. This affects the types returned by some methods used in `PowerSyncBackendConnector.UploadData`, namely `PowerSyncDatabase.GetNextCrudTransaction()` and `PowerSyncDatabase.GetCrudBatch()`.
+- Use `long` for op IDs instead of `string`. This affects the types returned by some methods used in `PowerSyncBackendConnector.UploadData`, namely `PowerSyncDatabase.GetNextCrudTransaction()` and `PowerSyncDatabase.GetCrudBatch()`, as well as `ICustomCheckpointRequestConnector.PostCheckpointRequest`.
+- Bundle and load native libraries on Android, iOS, and MacCatalyst. This allows `PowerSync.Common` to fully replace `PowerSync.Maui`.
## 1.0.0 (unlisted)
diff --git a/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs b/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs
index 2466bc14..56cd4131 100644
--- a/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs
+++ b/PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs
@@ -154,6 +154,10 @@ public class PowerSyncDatabase : IPowerSyncDatabase
protected IBucketStorageAdapter BucketStorageAdapter;
+ private readonly object currentStatusLock = new();
+ ///
+ /// The latest published sync status.
+ ///
public SyncStatus CurrentStatus { get; protected set; }
protected CancellationTokenSource masterCts = new();
@@ -163,6 +167,7 @@ public class PowerSyncDatabase : IPowerSyncDatabase
public ILogger Logger { get; protected set; }
private readonly AsyncLock runExclusive = new();
+
private readonly Func remoteFactory;
public StreamingSyncImplementation? SyncStreamImplementation => ConnectionManager.SyncStreamImplementation;
@@ -225,6 +230,8 @@ public PowerSyncDatabase(PowerSyncDatabaseOptions options)
await WaitForReady();
using (await runExclusive.LockAsync())
{
+ var syncStreamStatusCts = CancellationTokenSource.CreateLinkedTokenSource(masterCts.Token);
+
syncStreamImplementation = new StreamingSyncImplementation(new StreamingSyncImplementationOptions
{
Adapter = BucketStorageAdapter,
@@ -245,20 +252,22 @@ public PowerSyncDatabase(PowerSyncDatabaseOptions options)
Subscriptions = options.Subscriptions,
CrudUploadThrottleMs = options.CrudUploadThrottleMs,
TimeProvider = timeProvider,
- Logger = Logger
- });
-
- var syncStreamStatusCts = CancellationTokenSource.CreateLinkedTokenSource(masterCts.Token);
- var syncStreamStatusListener = syncStreamImplementation.Events.OnStatusChanged.ListenAsync(syncStreamStatusCts.Token);
- var _ = Task.Run(async () =>
- {
- await foreach (var update in syncStreamStatusListener)
+ Logger = Logger,
+ OnStatusChanged = status =>
{
- CurrentStatus = new SyncStatus(new SyncStatusOptions(update.Status.Options)
+ if (syncStreamStatusCts.IsCancellationRequested)
{
- HasSynced = CurrentStatus?.HasSynced == true || update.Status.LastSyncedAt != null,
- });
- Events.Emit(new PowerSyncDBEvents.StatusChangedEvent(CurrentStatus));
+ return;
+ }
+
+ lock (currentStatusLock)
+ {
+ CurrentStatus = new SyncStatus(new SyncStatusOptions(status.Options)
+ {
+ HasSynced = CurrentStatus?.HasSynced == true || status.LastSyncedAt != null,
+ });
+ Events.Emit(new PowerSyncDBEvents.StatusChangedEvent(CurrentStatus));
+ }
}
});
@@ -431,12 +440,16 @@ protected async Task ResolveOfflineSyncStatus()
var parsed = JsonConvert.DeserializeObject(result.r);
var parsedSyncStatus = CoreInstructionHelpers.CoreStatusToSyncStatus(parsed!);
- var updatedStatus = CurrentStatus.CreateUpdatedStatus(parsedSyncStatus);
- if (!updatedStatus.IsEqual(CurrentStatus))
+ lock (currentStatusLock)
{
- CurrentStatus = updatedStatus;
- Events.Emit(new PowerSyncDBEvents.StatusChangedEvent(CurrentStatus));
+ var updatedStatus = CurrentStatus.CreateUpdatedStatus(parsedSyncStatus);
+
+ if (!updatedStatus.IsEqual(CurrentStatus))
+ {
+ CurrentStatus = updatedStatus;
+ Events.Emit(new PowerSyncDBEvents.StatusChangedEvent(CurrentStatus));
+ }
}
}
@@ -531,8 +544,11 @@ await Database.WriteTransaction(async tx =>
});
// The data has been deleted - reset the sync status
- CurrentStatus = new SyncStatus(new SyncStatusOptions());
- Events.Emit(new PowerSyncDBEvents.StatusChangedEvent(CurrentStatus));
+ lock (currentStatusLock)
+ {
+ CurrentStatus = new SyncStatus(new SyncStatusOptions());
+ Events.Emit(new PowerSyncDBEvents.StatusChangedEvent(CurrentStatus));
+ }
}
///
diff --git a/PowerSync/PowerSync.Common/Client/Sync/Bucket/BucketStorageAdapter.cs b/PowerSync/PowerSync.Common/Client/Sync/Bucket/BucketStorageAdapter.cs
index 23a158e6..353c9c2b 100644
--- a/PowerSync/PowerSync.Common/Client/Sync/Bucket/BucketStorageAdapter.cs
+++ b/PowerSync/PowerSync.Common/Client/Sync/Bucket/BucketStorageAdapter.cs
@@ -8,6 +8,7 @@ namespace PowerSync.Common.Client.Sync.Bucket;
using PowerSync.Common.DB.Crud;
using PowerSync.Common.Utils;
+using PowerSync.Common.Utils.Converters;
public static class PowerSyncControlCommand
{
@@ -34,13 +35,15 @@ public static class PowerSyncControlConnectionState
public class Checkpoint
{
[JsonProperty("last_op_id")]
- public string LastOpId { get; set; } = null!;
+ [JsonConverter(typeof(StringLongConverter))]
+ public long LastOpId { get; set; }
[JsonProperty("buckets")]
public BucketChecksum[] Buckets { get; set; } = [];
[JsonProperty("write_checkpoint")]
- public string? WriteCheckpoint { get; set; } = null;
+ [JsonConverter(typeof(StringLongConverter))]
+ public long? WriteCheckpoint { get; set; } = null;
[JsonProperty("streams")]
public object[]? Streams { get; set; } = [];
diff --git a/PowerSync/PowerSync.Common/Client/Sync/Bucket/OplogEntry.cs b/PowerSync/PowerSync.Common/Client/Sync/Bucket/OplogEntry.cs
index f83f4a64..c89ff2ae 100644
--- a/PowerSync/PowerSync.Common/Client/Sync/Bucket/OplogEntry.cs
+++ b/PowerSync/PowerSync.Common/Client/Sync/Bucket/OplogEntry.cs
@@ -2,6 +2,8 @@ namespace PowerSync.Common.Client.Sync.Bucket;
using Newtonsoft.Json;
+using PowerSync.Common.Utils.Converters;
+
public class OplogEntryJSON
{
[JsonProperty("checksum")]
@@ -17,7 +19,8 @@ public class OplogEntryJSON
public string? ObjectType { get; set; }
[JsonProperty("op_id")]
- public string OpId { get; set; } = null!;
+ [JsonConverter(typeof(StringLongConverter))]
+ public long OpId { get; set; }
[JsonProperty("op")]
public string Op { get; set; } = null!;
@@ -27,7 +30,7 @@ public class OplogEntryJSON
}
public class OplogEntry(
- string opId,
+ long opId,
OpType op,
long checksum,
string subkey,
@@ -36,7 +39,7 @@ public class OplogEntry(
object? data = null
)
{
- public string OpId { get; private set; } = opId;
+ public long OpId { get; private set; } = opId;
public OpType Op { get; private set; } = op;
public long Checksum { get; private set; } = checksum;
public string Subkey { get; private set; } = subkey;
diff --git a/PowerSync/PowerSync.Common/Client/Sync/Bucket/SqliteBucketStorage.cs b/PowerSync/PowerSync.Common/Client/Sync/Bucket/SqliteBucketStorage.cs
index 21dbd193..75e92cd7 100644
--- a/PowerSync/PowerSync.Common/Client/Sync/Bucket/SqliteBucketStorage.cs
+++ b/PowerSync/PowerSync.Common/Client/Sync/Bucket/SqliteBucketStorage.cs
@@ -33,9 +33,10 @@ public SqliteBucketStorage(IDBAdapter db, ILogger? logger = null)
updateCts = new CancellationTokenSource();
+ var updates = db.Events.OnTablesUpdated.Listen(updateCts.Token);
updateTask = Task.Run(() =>
{
- foreach (var update in db.Events.OnTablesUpdated.Listen(updateCts.Token))
+ foreach (var update in updates)
{
var tables = DBAdapterUtils.ExtractTableUpdates(update.TablesUpdated);
if (tables.Contains(PSInternalTable.CRUD))
diff --git a/PowerSync/PowerSync.Common/Client/Sync/Stream/CoreInstructions.cs b/PowerSync/PowerSync.Common/Client/Sync/Stream/CoreInstructions.cs
index efc5adf7..c17c49c2 100644
--- a/PowerSync/PowerSync.Common/Client/Sync/Stream/CoreInstructions.cs
+++ b/PowerSync/PowerSync.Common/Client/Sync/Stream/CoreInstructions.cs
@@ -135,6 +135,7 @@ public class CoreSyncStatus
public List Streams { get; set; } = [];
[JsonProperty("internal_last_applied_checkpoint_request_id")]
+ [JsonConverter(typeof(StringLongConverter))]
public long? LastAppliedCheckpointRequestId { get; set; }
}
diff --git a/PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncImplementation.cs b/PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncImplementation.cs
index 3234950c..443d2e7d 100644
--- a/PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncImplementation.cs
+++ b/PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncImplementation.cs
@@ -13,6 +13,7 @@ namespace PowerSync.Common.Client.Sync.Stream;
using PowerSync.Common.Client.Sync.Bucket;
using PowerSync.Common.DB.Crud;
using PowerSync.Common.Utils;
+using PowerSync.Common.Utils.Converters;
public class AdditionalConnectionOptions(int? retryDelayMs = null, int? crudUploadThrottleMs = null)
{
@@ -61,6 +62,13 @@ public class StreamingSyncImplementationOptions : AdditionalConnectionOptions
public ILogger? Logger { get; init; }
+ ///
+ /// Called synchronously whenever the sync status changes, before the change is published on
+ /// . It runs while the status lock is held, so it
+ /// must not block or call back into the sync implementation.
+ ///
+ internal Action? OnStatusChanged { get; init; }
+
///
/// Source of the delays in the sync loops. Tests substitute a fake clock so they don't have to
/// wait out real retry delays.
@@ -193,8 +201,10 @@ public class StreamingSyncImplementation : ICloseable
///
/// The highest checkpoint request id the core extension has reported as applied, if any.
+ /// Updated in the download loop and read in the repost loop.
///
- internal long? LastAppliedCheckpointRequestId;
+ internal long? _lastAppliedCheckpointRequestId;
+ private readonly object _lastAppliedCheckpointLock = new();
private readonly ILogger logger;
private SubscribedStream[] activeStreams;
@@ -215,7 +225,7 @@ public class StreamingSyncImplementation : ICloseable
public StreamingSyncImplementation(StreamingSyncImplementationOptions options)
{
Options = options;
- SyncStatus = new SyncStatus(new SyncStatusOptions
+ _syncStatus = new SyncStatus(new SyncStatusOptions
{
Connected = false,
Connecting = false,
@@ -234,6 +244,23 @@ public StreamingSyncImplementation(StreamingSyncImplementationOptions options)
CancellationTokenSource = null;
}
+ private SyncStatus _syncStatus;
+ private readonly object _syncStatusLock = new();
+
+ ///
+ /// The current synchronization status.
+ ///
+ public SyncStatus SyncStatus
+ {
+ get
+ {
+ lock (_syncStatusLock)
+ {
+ return _syncStatus;
+ }
+ }
+ }
+
///
/// Indicates if the sync service is connected.
///
@@ -244,11 +271,6 @@ public StreamingSyncImplementation(StreamingSyncImplementationOptions options)
///
public DateTime? LastSyncedAt => SyncStatus.LastSyncedAt;
- ///
- /// The current synchronization status.
- ///
- public SyncStatus SyncStatus { get; protected set; }
-
public async Task Connect(PowerSyncConnectionOptions? options = null)
{
if (CancellationTokenSource != null)
@@ -260,25 +282,41 @@ public async Task Connect(PowerSyncConnectionOptions? options = null)
var tcs = new TaskCompletionSource();
var cts = new CancellationTokenSource();
- // Subscribe to events before starting StreamingSync to not miss the Connected == true event
+ // Subscribe to events before starting StreamingSync to not miss the first status change
var listener = Events.OnStatusChanged.ListenAsync(cts.Token);
streamingSyncTask = StreamingSync(CancellationTokenSource.Token, options);
+ // Resolves once the connection attempt has settled, which is when `Connecting` has been seen
+ // as true and became false again. A download error settles the attempt too: the download loop
+ // keeps retrying after one, and connecting shouldn't block for the whole retry loop.
var _ = Task.Run(async () =>
{
+ var sawStartOfConnection = false;
+
await foreach (var status in listener)
{
- if (status.Status.Connected == true)
+ if (status.Status.Connecting)
{
- tcs.TrySetResult(true);
- cts.Cancel();
- return;
+ sawStartOfConnection = true;
+ }
+
+ if (status.Status.DataFlowStatus.DownloadError != null)
+ {
+ logger.LogWarning("Initial connect attempt did not successfully connect to server");
+ }
+ else if (!sawStartOfConnection || status.Status.Connecting)
+ {
+ // Still connecting.
+ continue;
}
+
+ tcs.TrySetResult(true);
+ cts.Cancel();
+ return;
}
// Connection closed prematurely
- logger.LogWarning("Initial connect attempt did not successfully connect to server");
tcs.TrySetResult(true);
});
@@ -423,9 +461,10 @@ protected async Task DownloadLoop(CancellationToken signal, PowerSyncConnectionO
var retryDelayMs = options.RetryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
crudUpdateCts = new CancellationTokenSource();
+ var crudUpdates = Options.Adapter.Events.OnCrudUpdate.ListenAsync(crudUpdateCts.Token);
crudUpdateTask = Task.Run(async () =>
{
- await foreach (var _ in Options.Adapter.Events.OnCrudUpdate.ListenAsync(crudUpdateCts.Token))
+ await foreach (var _ in crudUpdates)
{
TriggerCrudUpload();
}
@@ -655,7 +694,10 @@ protected async Task RepostUnacknowledgedCheckpointRequests(CancellationToken si
///
internal bool IsCheckpointRequestApplied(long requestId)
{
- return LastAppliedCheckpointRequestId is not null && LastAppliedCheckpointRequestId >= requestId;
+ lock (_lastAppliedCheckpointLock)
+ {
+ return _lastAppliedCheckpointRequestId is not null && _lastAppliedCheckpointRequestId >= requestId;
+ }
}
protected record StreamingSyncIterationResult
@@ -838,7 +880,10 @@ async Task HandleInstruction(NonInterruptingInstruction instruction)
}
break;
case UpdateSyncStatus syncStatus:
- LastAppliedCheckpointRequestId = syncStatus.Status.LastAppliedCheckpointRequestId;
+ lock (_lastAppliedCheckpointLock)
+ {
+ _lastAppliedCheckpointRequestId = syncStatus.Status.LastAppliedCheckpointRequestId;
+ }
UpdateSyncStatus(CoreInstructionHelpers.CoreStatusToSyncStatusOptions(syncStatus.Status));
break;
case FetchCredentials fetchCredentials:
@@ -1197,40 +1242,50 @@ protected void UpdateSyncStatus(SyncStatusOptions options, UpdateSyncStatusOptio
{
try
{
- var updatedStatus = new SyncStatus(new SyncStatusOptions
+ lock (_syncStatusLock)
{
- Connected = options.Connected ?? SyncStatus.Connected,
- Connecting = !options.Connected.GetValueOrDefault() && (options.Connecting ?? SyncStatus.Connecting),
- LastSyncedAt = options.LastSyncedAt ?? SyncStatus.LastSyncedAt,
- PriorityStatusEntries = options.PriorityStatusEntries ?? SyncStatus.PriorityStatusEntries,
- DataFlow = new SyncDataFlowStatus
+ var updatedStatus = new SyncStatus(new SyncStatusOptions
{
- Uploading = options.DataFlow?.Uploading ?? SyncStatus.DataFlowStatus.Uploading,
- Downloading = options.DataFlow?.Downloading ?? SyncStatus.DataFlowStatus.Downloading,
- DownloadProgress = options.DataFlow?.DownloadProgress ?? SyncStatus.DataFlowStatus.DownloadProgress,
- DownloadError = updateOptions?.ClearDownloadError == true ? null : options.DataFlow?.DownloadError ?? SyncStatus.DataFlowStatus.DownloadError,
- UploadError = updateOptions?.ClearUploadError == true ? null : options.DataFlow?.UploadError ?? SyncStatus.DataFlowStatus.UploadError,
- InternalStreamSubscriptions = options.DataFlow?.InternalStreamSubscriptions ?? SyncStatus.DataFlowStatus.InternalStreamSubscriptions
- }
- });
+ Connected = options.Connected ?? _syncStatus.Connected,
+ Connecting = !options.Connected.GetValueOrDefault() && (options.Connecting ?? _syncStatus.Connecting),
+ LastSyncedAt = options.LastSyncedAt ?? _syncStatus.LastSyncedAt,
+ PriorityStatusEntries = options.PriorityStatusEntries ?? _syncStatus.PriorityStatusEntries,
+ DataFlow = new SyncDataFlowStatus
+ {
+ Uploading = options.DataFlow?.Uploading ?? _syncStatus.DataFlowStatus.Uploading,
+ Downloading = options.DataFlow?.Downloading ?? _syncStatus.DataFlowStatus.Downloading,
+ DownloadProgress = options.DataFlow?.DownloadProgress ?? _syncStatus.DataFlowStatus.DownloadProgress,
+ DownloadError = updateOptions?.ClearDownloadError == true ? null : options.DataFlow?.DownloadError ?? _syncStatus.DataFlowStatus.DownloadError,
+ UploadError = updateOptions?.ClearUploadError == true ? null : options.DataFlow?.UploadError ?? _syncStatus.DataFlowStatus.UploadError,
+ InternalStreamSubscriptions = options.DataFlow?.InternalStreamSubscriptions ?? _syncStatus.DataFlowStatus.InternalStreamSubscriptions
+ }
+ });
- if (!SyncStatus.Equals(updatedStatus))
- {
- SyncStatus = updatedStatus;
- logger.LogDebug("[Sync status changed]: {message}", updatedStatus.ToJSON());
+ // TODO: Always false because Equals is reference equality.
+ // A SyncStatus.IsEqual method exists, but is very inefficient.
+ if (!_syncStatus.Equals(updatedStatus))
+ {
+ _syncStatus = updatedStatus;
- // Emit events using new SyncStatus objects to prevent local modifications propagating to StreamingSyncImplementation
+ // Suppress CA1873 (expensive arguments to logging function)
+ if (logger.IsEnabled(LogLevel.Debug))
+ logger.LogDebug("[Sync status changed]: {message}", updatedStatus.ToJSON());
- // Only trigger this if there was a change
- Events.Emit(new StreamingSyncImplementationEvents.StatusChangedEvent(new SyncStatus(updatedStatus.Options)));
+ // Emit events using new _syncStatus objects to prevent local modifications propagating to StreamingSyncImplementation
- // Emit StatusUpdated event wrapping a new SyncStatus object (prevents race conditions)
- Events.Emit(new StreamingSyncImplementationEvents.StatusUpdatedEvent(new SyncStatus(updatedStatus.Options)));
- }
- else
- {
- // Emit StatusUpdated event directly wrapping `updatedStatus` (not exposed elsewhere)
- Events.Emit(new StreamingSyncImplementationEvents.StatusUpdatedEvent(updatedStatus));
+ Options.OnStatusChanged?.Invoke(new SyncStatus(updatedStatus.Options));
+
+ // Only trigger this if there was a change
+ Events.Emit(new StreamingSyncImplementationEvents.StatusChangedEvent(new SyncStatus(updatedStatus.Options)));
+
+ // Emit StatusUpdated event wrapping a new SyncStatus object (prevents race conditions)
+ Events.Emit(new StreamingSyncImplementationEvents.StatusUpdatedEvent(new SyncStatus(updatedStatus.Options)));
+ }
+ else
+ {
+ // Emit StatusUpdated event directly wrapping `updatedStatus` (not exposed elsewhere)
+ Events.Emit(new StreamingSyncImplementationEvents.StatusUpdatedEvent(updatedStatus));
+ }
}
}
catch (Exception ex)
@@ -1305,7 +1360,7 @@ private static double WithJitter(int delayMs)
}
internal record LegacyWriteCheckpointResponseData(
- [property: JsonProperty("write_checkpoint")] long WriteCheckpoint
+ [property: JsonProperty("write_checkpoint"), JsonConverter(typeof(StringLongConverter))] long WriteCheckpoint
);
internal record LegacyWriteCheckpointApiResponse(
[property: JsonProperty("data")] LegacyWriteCheckpointResponseData Data
diff --git a/PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncTypes.cs b/PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncTypes.cs
index 71694c77..11c501cc 100644
--- a/PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncTypes.cs
+++ b/PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncTypes.cs
@@ -1,5 +1,6 @@
using PowerSync.Common.Client.Sync.Bucket;
using PowerSync.Common.DB.Crud;
+using PowerSync.Common.Utils.Converters;
using Newtonsoft.Json;
@@ -128,6 +129,7 @@ public class StreamingSyncCheckpointDiff : StreamingSyncLine
public class CheckpointDiff
{
[JsonProperty("last_op_id")]
+ [JsonConverter(typeof(StringLongConverter))]
public long LastOpId { get; set; }
[JsonProperty("updated_buckets")]
@@ -137,6 +139,7 @@ public class CheckpointDiff
public List RemovedBuckets { get; set; } = [];
[JsonProperty("write_checkpoint")]
+ [JsonConverter(typeof(StringLongConverter))]
public long WriteCheckpoint { get; set; }
}
@@ -197,6 +200,7 @@ public class CheckpointRequestPayload
public string ClientId { get; set; } = "";
[JsonProperty("checkpoint_request_id")]
+ [JsonConverter(typeof(StringLongConverter))]
public long CheckpointRequestId { get; set; }
}
@@ -209,5 +213,6 @@ public class CheckpointRequestResponse
public class CheckpointRequestResponseData
{
[JsonProperty("checkpoint_request_id")]
+ [JsonConverter(typeof(StringLongConverter))]
public long CheckpointRequestId { get; set; }
}
diff --git a/PowerSync/PowerSync.Common/Client/WatchManager.cs b/PowerSync/PowerSync.Common/Client/WatchManager.cs
index 4df2210b..cbe1f4da 100644
--- a/PowerSync/PowerSync.Common/Client/WatchManager.cs
+++ b/PowerSync/PowerSync.Common/Client/WatchManager.cs
@@ -32,9 +32,12 @@ public WatchManager(PowerSyncDatabase db, CancellationToken masterToken)
this.db = db;
this.masterToken = masterToken;
+ var tableUpdates = db.Database.Events.OnTablesUpdated.ListenAsync(masterToken);
+ var schemaChanges = db.Events.OnSchemaChanged.ListenAsync(masterToken);
+
// Dispatch two tasks, one for TableUpdated events and one for SchemaChanged events
- _ = Task.Run(RunTableUpdateLoop);
- _ = Task.Run(RunSchemaChangeLoop);
+ _ = Task.Run(() => RunTableUpdateLoop(tableUpdates));
+ _ = Task.Run(() => RunSchemaChangeLoop(schemaChanges));
}
public IAsyncEnumerable Watch(string sql, object?[]? parameters, SQLWatchOptions? options)
@@ -138,11 +141,11 @@ private async IAsyncEnumerable Stream(WatchSubscription subscription, Func
}
}
- private async Task RunTableUpdateLoop()
+ private async Task RunTableUpdateLoop(IAsyncEnumerable updates)
{
try
{
- await foreach (var update in db.Database.Events.OnTablesUpdated.ListenAsync(masterToken))
+ await foreach (var update in updates)
{
// Prevent a single bad notification from taking down the entire TablesUpdated loop
try
@@ -176,11 +179,11 @@ private async Task RunTableUpdateLoop()
}
}
- private async Task RunSchemaChangeLoop()
+ private async Task RunSchemaChangeLoop(IAsyncEnumerable schemaChanges)
{
try
{
- await foreach (var _ in db.Events.OnSchemaChanged.ListenAsync(masterToken))
+ await foreach (var _ in schemaChanges)
{
foreach (var entry in watches)
{
@@ -196,18 +199,7 @@ private async Task RunSchemaChangeLoop()
}
private static HashSet ExpandTableNames(IEnumerable tables) =>
- [.. tables.SelectMany(table => new[] { $"{PS_DATA_PREFIX}{table}", $"{PS_DATA_LOCAL_PREFIX}{table}" })];
-
- private static string InternalToFriendlyTableName(string internalName)
- {
- if (internalName.StartsWith(PS_DATA_PREFIX))
- return internalName.Substring(PS_DATA_PREFIX.Length);
-
- if (internalName.StartsWith(PS_DATA_LOCAL_PREFIX))
- return internalName.Substring(PS_DATA_LOCAL_PREFIX.Length);
-
- return internalName;
- }
+ [.. tables.SelectMany(table => new[] { $"{PS_DATA_PREFIX}{table}", $"{PS_DATA_LOCAL_PREFIX}{table}", $"{table}" })];
}
///
diff --git a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs
index cdb4ec9e..6055c929 100644
--- a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs
+++ b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs
@@ -12,6 +12,10 @@ namespace PowerSync.Common.MDSQLite;
using PowerSync.Common.DB;
using PowerSync.Common.Utils;
+#if IOS || MACCATALYST
+using Foundation;
+#endif
+
public class MDSQLiteAdapterOptions()
{
public string Name { get; set; } = null!;
@@ -111,9 +115,10 @@ private async Task Init()
// Register TablesUpdated listener
tablesUpdatedCts = new CancellationTokenSource();
+ var notifications = writeConnection.ListenAsync(tablesUpdatedCts.Token);
tablesUpdatedTask = Task.Run(async () =>
{
- await foreach (var notification in writeConnection.ListenAsync(tablesUpdatedCts.Token))
+ await foreach (var notification in notifications)
{
if (notification.TablesUpdated != null)
{
@@ -154,13 +159,30 @@ protected virtual void LoadExtensions(SqliteConnection db)
///
/// Loads the bundled PowerSync core SQLite extension. Override on
- /// platform-specific adapters (e.g. MAUI iOS/Android) where the native library
- /// lives outside the desktop runtime path.
+ /// platform-specific adapters where the native library lives
+ /// outside the standard runtimes path.
///
protected virtual void LoadDefaultPowerSyncExtension(SqliteConnection db)
{
- var path = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory);
- db.LoadExtension(path, "sqlite3_powersync_init");
+ string extensionPath;
+
+#if IOS || MACCATALYST
+ var bundlePath =
+ NSBundle.FromIdentifier("co.powersync.sqlitecore")?.BundlePath
+ ?? throw new Exception("Could not find PowerSync SQLite extension bundle path");
+ extensionPath = Path.Combine(bundlePath, "powersync-sqlite-core");
+#elif ANDROID
+ extensionPath = "libpowersync";
+#else
+ extensionPath = PowerSyncDesktopPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory);
+#endif
+
+ // Use manual command instead of db.LoadExtension(), since that has issues on iOS
+ using var loadExtension = db.CreateCommand();
+ loadExtension.CommandText = "SELECT load_extension(@path, @entryPoint)";
+ loadExtension.Parameters.AddWithValue("@path", extensionPath);
+ loadExtension.Parameters.AddWithValue("@entryPoint", "sqlite3_powersync_init");
+ loadExtension.ExecuteNonQuery();
}
public async Task Close()
diff --git a/PowerSync/PowerSync.Common/PowerSync.Common.csproj b/PowerSync/PowerSync.Common/PowerSync.Common.csproj
index 01794909..6fb77ba4 100644
--- a/PowerSync/PowerSync.Common/PowerSync.Common.csproj
+++ b/PowerSync/PowerSync.Common/PowerSync.Common.csproj
@@ -39,26 +39,39 @@
+
-
-
+
-
-
-
-
- PreserveNewest
-
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
diff --git a/PowerSync/PowerSync.Common/PowerSync.Common.targets b/PowerSync/PowerSync.Common/PowerSync.Common.targets
deleted file mode 100644
index 0c5bcba2..00000000
--- a/PowerSync/PowerSync.Common/PowerSync.Common.targets
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/PowerSync/PowerSync.Common/Utils/Converters/StringLongConverter.cs b/PowerSync/PowerSync.Common/Utils/Converters/StringLongConverter.cs
index 61c7cbdf..73f092ed 100644
--- a/PowerSync/PowerSync.Common/Utils/Converters/StringLongConverter.cs
+++ b/PowerSync/PowerSync.Common/Utils/Converters/StringLongConverter.cs
@@ -6,10 +6,6 @@ namespace PowerSync.Common.Utils.Converters;
/// Converts a long to and from a string when converting JSON values. Used
/// for converting checkpoint request IDs from a long to a string before being
/// passed to the core extension.
-///
-/// TODO: This is not currently in use because checkpoint request IDs are
-/// currently represented as strings, however this is going to change
-/// in the 1.0 release.
///
internal class StringLongConverter : JsonConverter
{
@@ -42,6 +38,6 @@ public override object ReadJson(JsonReader reader, Type objectType, object? exis
return result;
}
- throw new JsonSerializationException($"Cannot convert value {val} to long.");
+ throw new JsonSerializationException($"Cannot convert value '{val}' to long.");
}
}
diff --git a/PowerSync/PowerSync.Common/Utils/PowerSyncPathResolver.cs b/PowerSync/PowerSync.Common/Utils/PowerSyncDesktopPathResolver.cs
similarity index 97%
rename from PowerSync/PowerSync.Common/Utils/PowerSyncPathResolver.cs
rename to PowerSync/PowerSync.Common/Utils/PowerSyncDesktopPathResolver.cs
index 8aa7c3a4..f6c07f77 100644
--- a/PowerSync/PowerSync.Common/Utils/PowerSyncPathResolver.cs
+++ b/PowerSync/PowerSync.Common/Utils/PowerSyncDesktopPathResolver.cs
@@ -2,7 +2,7 @@ namespace PowerSync.Common.Utils;
using System.Runtime.InteropServices;
-public static class PowerSyncPathResolver
+public static class PowerSyncDesktopPathResolver
{
public static string GetNativeLibraryPath(string packagePath)
{
diff --git a/PowerSync/PowerSync.Common/buildTransitive/PowerSync.Common.targets b/PowerSync/PowerSync.Common/buildTransitive/PowerSync.Common.targets
new file mode 100644
index 00000000..4f1aa9d5
--- /dev/null
+++ b/PowerSync/PowerSync.Common/buildTransitive/PowerSync.Common.targets
@@ -0,0 +1,21 @@
+
+
+
+
+ <_PowerSyncUnusableRuntimeTarget Include="@(RuntimeTargetsCopyLocalItems)"
+ Condition="'%(NuGetPackageId)' == 'PowerSync.Common' And !$([System.Text.RegularExpressions.Regex]::IsMatch('%(RuntimeIdentifier)', '^(win|linux|osx)'))" />
+
+
+
+
diff --git a/PowerSync/PowerSync.Maui/CHANGELOG.md b/PowerSync/PowerSync.Maui/CHANGELOG.md
index 72f1d515..5cb0969c 100644
--- a/PowerSync/PowerSync.Maui/CHANGELOG.md
+++ b/PowerSync/PowerSync.Maui/CHANGELOG.md
@@ -2,8 +2,7 @@
## 1.0.1
-- Full release.
-- Upstream PowerSync.Common version bump (See Powersync.Common changelog 1.0.1 for more information)
+- ⚠️ Deprecate `PowerSync.Maui`. Use `PowerSync.Common` and `MDSQLiteAdapter` / `MDSQLiteDBOpenFactory` instead.
## 1.0.0 (unlisted)
diff --git a/PowerSync/PowerSync.Maui/PowerSync.Maui.csproj b/PowerSync/PowerSync.Maui/PowerSync.Maui.csproj
index f8864efb..552547ce 100644
--- a/PowerSync/PowerSync.Maui/PowerSync.Maui.csproj
+++ b/PowerSync/PowerSync.Maui/PowerSync.Maui.csproj
@@ -1,5 +1,4 @@
-
net9.0;net10.0;net9.0-android;net10.0-android
$(TargetFrameworks);net9.0-ios;net10.0-ios;net9.0-maccatalyst;net10.0-maccatalyst
@@ -19,7 +18,6 @@
https://github.com/powersync-ja/powersync-dotnet/PowerSync/PowerSync.Maui/CHANGELOG.md
powersync local-first local-storage state-management offline sql db persistence sqlite sync
icon.png
- NU5100;1591
README.md
true
true
@@ -27,44 +25,11 @@
-
+
-
- Designer
-
-
-
-
-
-
-
-
- Framework
- False
-
-
-
-
-
-
- Framework
- False
-
-
-
-
-
-
-
-
-
-
diff --git a/PowerSync/PowerSync.Maui/README.md b/PowerSync/PowerSync.Maui/README.md
index bc75882c..af6137a4 100644
--- a/PowerSync/PowerSync.Maui/README.md
+++ b/PowerSync/PowerSync.Maui/README.md
@@ -1,45 +1,3 @@
# PowerSync SDK .NET MAUI
-This package provides .NET Multi-platform App UI (MAUI) integration for PowerSync, designed to work with the PowerSync.Common package for cross-platform mobile and desktop applications.
-
-## ⚠️ Project Status & Release Note
-
-This package is in beta and is considered ready for production use for tested use cases. See our feature status definitions [here](https://docs.powersync.com/resources/feature-status).
-
-## Installation
-
-This package is published on [NuGet](https://www.nuget.org/packages/PowerSync.Maui) and requires PowerSync.Common to also be installed.
-
-```bash
-dotnet add package PowerSync.Maui
-dotnet add package PowerSync.Common
-```
-
-## Usage
-
-Initialization differs slightly from our Common SDK when using MAUI.
-
-```csharp
-
-private record ListResult(string id, string name, string owner_id, string created_at);
-
-static async Task Main() {
-
- // Ensures the DB file is stored in a platform appropriate location
- var dbPath = Path.Combine(FileSystem.AppDataDirectory, "maui-example.db");
- var factory = new MAUISQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions()
- {
- DbFilename = dbPath
- });
-
- var Db = new PowerSyncDatabase(new PowerSyncDatabaseOptions()
- {
- Database = factory, // Supply a factory
- Schema = AppSchema.PowerSyncSchema,
- });
-
- await db.Init();
-
- var lists = await db.GetAll("select * from lists");
-}
-```
+- ⚠️ This package is deprecated. Use `PowerSync.Common` and `MDSQLiteAdapter` / `MDSQLiteDBOpenFactory` instead.
diff --git a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs b/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs
deleted file mode 100644
index 0d6e3993..00000000
--- a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-namespace PowerSync.Maui.SQLite;
-
-using Microsoft.Data.Sqlite;
-
-using PowerSync.Common.MDSQLite;
-
-// iOS/MacCatalyst specific imports
-#if IOS || MACCATALYST
-using Foundation;
-#endif
-
-public class MAUISQLiteAdapter : MDSQLiteAdapter
-{
- public MAUISQLiteAdapter(MDSQLiteAdapterOptions options) : base(options)
- {
- }
-
- // The bundled PowerSync extension lives in a platform-specific location on
- // iOS/MacCatalyst/Android — the desktop runtime path used by the base class
- // does not resolve to it. Override only the PowerSync-extension load hook;
- // user-supplied custom extensions still flow through MDSQLiteAdapter.LoadExtensions
- // unchanged, so consumers can freely combine the bundled extension (via the
- // LoadPowerSyncExtension flag) with their own.
- protected override void LoadDefaultPowerSyncExtension(SqliteConnection db)
- {
-#if IOS || MACCATALYST
- LoadExtensionApple(db);
-#elif ANDROID
- db.LoadExtension("libpowersync");
-#else
- base.LoadDefaultPowerSyncExtension(db);
-#endif
- }
-
- private static void LoadExtensionApple(SqliteConnection db)
- {
-#if IOS || MACCATALYST
- var bundlePath = Foundation.NSBundle.FromIdentifier("co.powersync.sqlitecore")?.BundlePath;
- if (bundlePath == null)
- {
- throw new Exception("Could not find PowerSync SQLite extension bundle path");
- }
-
- var filePath =
- Path.Combine(bundlePath, "powersync-sqlite-core");
-
- using var loadExtension = db.CreateCommand();
- loadExtension.CommandText = "SELECT load_extension(@path, @entryPoint)";
- loadExtension.Parameters.AddWithValue("@path", filePath);
- loadExtension.Parameters.AddWithValue("@entryPoint", "sqlite3_powersync_init");
- loadExtension.ExecuteNonQuery();
-#endif
- }
-}
-
diff --git a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteDBOpenFactory.cs b/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteDBOpenFactory.cs
deleted file mode 100644
index c8d35088..00000000
--- a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteDBOpenFactory.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using PowerSync.Common.DB;
-
-namespace PowerSync.Maui.SQLite;
-
-using PowerSync.Common.Client;
-using PowerSync.Common.MDSQLite;
-
-
-public class MAUISQLiteDBOpenFactory : ISQLOpenFactory
-{
- private readonly MDSQLiteOpenFactoryOptions options;
-
- public MAUISQLiteDBOpenFactory(MDSQLiteOpenFactoryOptions options)
- {
- this.options = options;
- }
-
- public IDBAdapter OpenDatabase()
- {
- return new MAUISQLiteAdapter(new MDSQLiteAdapterOptions
- {
- Name = options.DbFilename,
- SqliteOptions = options.SqliteOptions
- });
- }
-}
diff --git a/PowerSync/PowerSync.Maui/build/ApiDefinition.cs b/PowerSync/PowerSync.Maui/build/ApiDefinition.cs
deleted file mode 100644
index 5fe8ef43..00000000
--- a/PowerSync/PowerSync.Maui/build/ApiDefinition.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace PowerSync.Maui.build
-{
- // Empty API definition - allows xcframework to be included without managed bindings
-}
diff --git a/README.md b/README.md
index a48185b4..af113abf 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,6 @@ Packages are published to [NuGet](https://www.nuget.org/profiles/PowerSync).
- [PowerSync.Common](./PowerSync/PowerSync.Common/README.md)
- Core package: .NET implementation of a PowerSync database connector and streaming sync bucket implementation. Packages meant for specific platforms will extend functionality of `Common`.
-- [PowerSync.Maui](./PowerSync/PowerSync.Maui/README.md)
- - Extends the PowerSync.Common package to provide the .NET Multi-platform App UI (MAUI) integration for PowerSync for cross-platform mobile and desktop applications.
## Demo Apps / Example Projects
@@ -31,41 +29,9 @@ Demo applications are located in the [`demos/`](./demos/) directory. Also see ou
This PowerSync SDK supports the following target frameworks:
-- **.NET 9** - [Latest version](https://dotnet.microsoft.com/en-us/download/dotnet/9.0)
-- **.NET 8** - [Current LTS Version, used for development of this project](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
-- **.NET 6** - supported for compatibility with older projects
-- **.NET Standard 2.0** - for compatibility with older libraries and frameworks, tested/verified older versions will be listed below.
-
-- .NET Framework 4.8:
-
- To get a .NET Framework 4.8 working with this SDK add the following to your `.csproj` file:
-
- ```xml
-
- ...
-
- win-x86;win-x64
- win-x64
-
-
-
- ...
-
-
-
- ```
-
- and create a `IsExternalInit.cs` file in your project with the following contents:
-
- ```cs
- using System.ComponentModel;
-
- namespace System.Runtime.CompilerServices
- {
- [EditorBrowsable(EditorBrowsableState.Never)]
- internal class IsExternalInit { }
- }
- ```
+- **.NET 10** - [Current LTS Version, used for development of this project](https://dotnet.microsoft.com/en-us/download/dotnet/10.0)
+- **.NET 9** - [Current STS Version](https://dotnet.microsoft.com/en-us/download/dotnet/9.0)
+- **.NET 8** - [Previous LTS Version, supported until November 10, 2026](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
---
diff --git a/Tests/PowerSync/PowerSync.Common.Tests/Client/PowerSyncDatabaseTests.cs b/Tests/PowerSync/PowerSync.Common.Tests/Client/PowerSyncDatabaseTests.cs
index 840cbfd2..8909886d 100644
--- a/Tests/PowerSync/PowerSync.Common.Tests/Client/PowerSyncDatabaseTests.cs
+++ b/Tests/PowerSync/PowerSync.Common.Tests/Client/PowerSyncDatabaseTests.cs
@@ -302,7 +302,7 @@ await db.Execute(
Assert.All(ids, n => Assert.Equal(id, n));
}
- [Fact(Timeout = 2000)]
+ [Fact(Timeout = 5000)]
public async Task ReadWhileWriteIsRunning()
{
var sem = new TaskCompletionSource();
@@ -357,7 +357,7 @@ public async Task BatchExecute()
Assert.Equal(make2, result[1].make);
}
- [Fact(Timeout = 2000)]
+ [Fact(Timeout = 5000)]
public async Task QueueSimultaneousExecutions()
{
var order = new List();
@@ -376,7 +376,7 @@ await db.WriteLock(async context =>
Assert.Equal(expectedOrder, order);
}
- [Fact(Timeout = 2000)]
+ [Fact(Timeout = 5000)]
public async Task CallUpdateHookOnChanges()
{
var result = new TaskCompletionSource();
@@ -401,7 +401,7 @@ public async Task CallUpdateHookOnChanges()
await result.Task;
}
- [Fact(Timeout = 2000)]
+ [Fact(Timeout = 5000)]
public async Task ReflectWriteTransactionUpdatesOnReadConnections()
{
var watched = new TaskCompletionSource();
@@ -584,7 +584,7 @@ await db.Execute(
Assert.Equal(make, dynamicAsset?.make);
}
- [Fact(Timeout = 2000)]
+ [Fact(Timeout = 5000)]
public async Task Watch_Cancelled()
{
int callCount = 0;
@@ -616,7 +616,7 @@ public async Task Watch_Cancelled()
Assert.Equal(2, callCount);
}
- [Fact(Timeout = 3000)]
+ [Fact(Timeout = 5000)]
public async Task Watch_MultipleCancelled()
{
int callCount = 0;
@@ -667,7 +667,7 @@ void RunQuery(CancellationTokenSource cts, SemaphoreSlim sem)
Assert.Equal(3, callCount);
}
- [Fact(Timeout = 3000)]
+ [Fact(Timeout = 5000)]
public async Task Watch_SchemaReset()
{
var dbId = Guid.NewGuid().ToString();
@@ -842,7 +842,7 @@ public async Task Watch_TriggerImmediately_False()
Assert.Equal(3, current[0].count);
}
- [Fact(Timeout = 2000)]
+ [Fact(Timeout = 5000)]
public async Task Watch_CancelsOnTokenCancellation()
{
var tcs = new TaskCompletionSource();
diff --git a/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/CheckpointRequestsTests.cs b/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/CheckpointRequestsTests.cs
index 01227bfb..50f149c7 100644
--- a/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/CheckpointRequestsTests.cs
+++ b/Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/CheckpointRequestsTests.cs
@@ -68,9 +68,9 @@ public async Task CheckpointRequests_RequestsCheckpointsForUpdates()
{
Checkpoint = new()
{
- LastOpId = "1",
+ LastOpId = 1,
Buckets = [MockDataFactory.Bucket("a", 1, subscriptions: Array.Empty