Skip to content

Commit 00d56c7

Browse files
authored
Merge pull request #7 from powersync-ja/feat/api-catchup-effort-1
feat: Expose upload/download errors, credential management, last synced value from core extension
2 parents 5598f67 + 7cbfdb8 commit 00d56c7

File tree

7 files changed

+203
-21
lines changed

7 files changed

+203
-21
lines changed

PowerSync/PowerSync.Common/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.0.2-alpha.2
2+
3+
- Updated core extension to v0.3.14
4+
- Loading last synced time from core extension
5+
- Expose upload and download errors on SyncStatus
6+
- Improved credentials management and error handling. Credentials are invalidated when they expire or become invalid based on responses from the PowerSync service. The frequency of credential fetching has been reduced as a result of this work.
7+
18
## 0.0.2-alpha.1
29

310
- Introduce package. Support for Desktop .NET use cases.

PowerSync/PowerSync.Common/Client/PowerSyncDatabase.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public interface IPowerSyncDatabase : IEventStream<PowerSyncDBEvent>
8585

8686
public class PowerSyncDatabase : EventStream<PowerSyncDBEvent>, IPowerSyncDatabase
8787
{
88+
private static readonly int FULL_SYNC_PRIORITY = 2147483647;
8889

8990
public IDBAdapter Database;
9091
private Schema schema;
@@ -231,21 +232,35 @@ private async Task LoadVersion()
231232
}
232233
}
233234

234-
private record LastSyncedResult(string? synced_at);
235+
private record LastSyncedResult(int? priority, string? last_synced_at);
235236

236237
protected async Task UpdateHasSynced()
237238
{
238-
var result = await Database.Get<LastSyncedResult>("SELECT powersync_last_synced_at() as synced_at");
239+
var results = await Database.GetAll<LastSyncedResult>(
240+
"SELECT priority, last_synced_at FROM ps_sync_state ORDER BY priority DESC"
241+
);
239242

240-
var hasSynced = result.synced_at != null;
241-
DateTime? syncedAt = result.synced_at != null ? DateTime.Parse(result.synced_at + "Z") : null;
243+
DateTime? lastCompleteSync = null;
244+
245+
// TODO: Will be altered/extended when reporting individual sync priority statuses is supported
246+
foreach (var result in results)
247+
{
248+
var parsedDate = DateTime.Parse(result.last_synced_at + "Z");
249+
250+
if (result.priority == FULL_SYNC_PRIORITY)
251+
{
252+
// This lowest-possible priority represents a complete sync.
253+
lastCompleteSync = parsedDate;
254+
}
255+
}
242256

257+
var hasSynced = lastCompleteSync != null;
243258
if (hasSynced != CurrentStatus.HasSynced)
244259
{
245260
CurrentStatus = new SyncStatus(new SyncStatusOptions(CurrentStatus.Options)
246261
{
247262
HasSynced = hasSynced,
248-
LastSyncedAt = syncedAt
263+
LastSyncedAt = lastCompleteSync,
249264
});
250265

251266
Emit(new PowerSyncDBEvent { StatusChanged = CurrentStatus });

PowerSync/PowerSync.Common/Client/Sync/Stream/Remote.cs

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace PowerSync.Common.Client.Sync.Stream;
66
using System.Text;
77
using System.Threading;
88
using System.Threading.Tasks;
9+
using System.Text.RegularExpressions;
910

1011
using Newtonsoft.Json;
1112
using Newtonsoft.Json.Linq;
@@ -29,7 +30,6 @@ public class RequestDetails
2930

3031
public class Remote
3132
{
32-
private static int REFRESH_CREDENTIALS_SAFETY_PERIOD_MS = 30_000;
3333
private readonly HttpClient httpClient;
3434
protected IPowerSyncBackendConnector connector;
3535

@@ -41,18 +41,48 @@ public Remote(IPowerSyncBackendConnector connector)
4141
this.connector = connector;
4242
}
4343

44+
/// <summary>
45+
/// Get credentials currently cached, or fetch new credentials if none are available.
46+
/// These credentials may have expired already.
47+
/// </summary>
4448
public async Task<PowerSyncCredentials?> GetCredentials()
4549
{
46-
if (credentials?.ExpiresAt > DateTime.Now.AddMilliseconds(REFRESH_CREDENTIALS_SAFETY_PERIOD_MS))
50+
if (credentials != null)
4751
{
4852
return credentials;
4953
}
54+
return await PrefetchCredentials();
55+
}
5056

51-
credentials = await connector.FetchCredentials();
52-
57+
/// <summary>
58+
/// Fetch a new set of credentials and cache it.
59+
/// Until this call succeeds, GetCredentials will still return the old credentials.
60+
/// This may be called before the current credentials have expired.
61+
/// </summary>
62+
public async Task<PowerSyncCredentials?> PrefetchCredentials()
63+
{
64+
credentials = await FetchCredentials();
5365
return credentials;
5466
}
5567

68+
/// <summary>
69+
/// Get credentials for PowerSync.
70+
/// This should always fetch a fresh set of credentials - don't use cached values.
71+
/// </summary>
72+
public async Task<PowerSyncCredentials?> FetchCredentials()
73+
{
74+
return await connector.FetchCredentials();
75+
}
76+
77+
/// <summary>
78+
/// Immediately invalidate credentials.
79+
/// This may be called when the current credentials have expired.
80+
/// </summary>
81+
public void InvalidateCredentials()
82+
{
83+
credentials = null;
84+
}
85+
5686
static string GetUserAgent()
5787
{
5888
object[] attributes = Assembly.GetExecutingAssembly()
@@ -76,6 +106,11 @@ public async Task<T> Get<T>(string path, Dictionary<string, string>? headers = n
76106
using var client = new HttpClient();
77107
var response = await client.SendAsync(request);
78108

109+
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
110+
{
111+
InvalidateCredentials();
112+
}
113+
79114
if (!response.IsSuccessStatusCode)
80115
{
81116
var errorMessage = await response.Content.ReadAsStringAsync();
@@ -95,7 +130,12 @@ public async Task<T> Get<T>(string path, Dictionary<string, string>? headers = n
95130
{
96131
throw new HttpRequestException($"HTTP {response.StatusCode}: No content");
97132
}
98-
else
133+
134+
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
135+
{
136+
InvalidateCredentials();
137+
}
138+
99139
if (!response.IsSuccessStatusCode)
100140
{
101141
var errorText = await response.Content.ReadAsStringAsync();

PowerSync/PowerSync.Common/Client/Sync/Stream/StreamingSyncImplementation.cs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ protected async Task StreamingSync(CancellationToken? signal, PowerSyncConnectio
297297
UpdateSyncStatus(new SyncStatusOptions
298298
{
299299
Connected = false,
300+
DataFlow = new SyncDataFlowStatus
301+
{
302+
DownloadError = ex
303+
}
300304
});
301305

302306
// On error, wait a little before retrying
@@ -466,7 +470,13 @@ protected async Task<StreamingSyncIterationResult> StreamingSyncIteration(Cancel
466470
{
467471
Connected = true,
468472
LastSyncedAt = DateTime.Now,
469-
DataFlow = new SyncDataFlowStatus { Downloading = false }
473+
DataFlow = new SyncDataFlowStatus
474+
{
475+
Downloading = false
476+
}
477+
}, new UpdateSyncStatusOptions
478+
{
479+
ClearDownloadError = true
470480
});
471481

472482
}
@@ -539,13 +549,21 @@ protected async Task<StreamingSyncIterationResult> StreamingSyncIteration(Cancel
539549
{
540550
// Connection would be closed automatically right after this
541551
logger.LogDebug("Token expiring; reconnect");
552+
Options.Remote.InvalidateCredentials();
542553

543554
// For a rare case where the backend connector does not update the token
544555
// (uses the same one), this should have some delay.
545556
//
546557
await DelayRetry();
547558
return new StreamingSyncIterationResult { Retry = true };
548559
}
560+
else if (remainingSeconds < 30)
561+
{
562+
logger.LogDebug("Token will expire soon; reconnect");
563+
// Pre-emptively refresh the token
564+
Options.Remote.InvalidateCredentials();
565+
return new StreamingSyncIterationResult { Retry = true };
566+
}
549567
TriggerCrudUpload();
550568
}
551569
else
@@ -557,8 +575,13 @@ protected async Task<StreamingSyncIterationResult> StreamingSyncIteration(Cancel
557575
UpdateSyncStatus(new SyncStatusOptions
558576
{
559577
Connected = true,
560-
LastSyncedAt = DateTime.Now
561-
});
578+
LastSyncedAt = DateTime.Now,
579+
},
580+
new UpdateSyncStatusOptions
581+
{
582+
ClearDownloadError = true
583+
}
584+
);
562585
}
563586
else if (validatedCheckpoint == targetCheckpoint)
564587
{
@@ -584,8 +607,12 @@ protected async Task<StreamingSyncIterationResult> StreamingSyncIteration(Cancel
584607
LastSyncedAt = DateTime.Now,
585608
DataFlow = new SyncDataFlowStatus
586609
{
587-
Downloading = false
610+
Downloading = false,
588611
}
612+
},
613+
new UpdateSyncStatusOptions
614+
{
615+
ClearDownloadError = true
589616
});
590617
}
591618
}
@@ -655,6 +682,14 @@ await locks.ObtainLock(new LockOptions<Task>
655682

656683
checkedCrudItem = nextCrudItem;
657684
await Options.UploadCrud();
685+
UpdateSyncStatus(new SyncStatusOptions
686+
{
687+
},
688+
new UpdateSyncStatusOptions
689+
{
690+
ClearUploadError = true
691+
});
692+
658693
}
659694
else
660695
{
@@ -666,7 +701,14 @@ await locks.ObtainLock(new LockOptions<Task>
666701
catch (Exception ex)
667702
{
668703
checkedCrudItem = null;
669-
UpdateSyncStatus(new SyncStatusOptions { DataFlow = new SyncDataFlowStatus { Uploading = false } });
704+
UpdateSyncStatus(new SyncStatusOptions
705+
{
706+
DataFlow = new SyncDataFlowStatus
707+
{
708+
Uploading = false,
709+
UploadError = ex
710+
}
711+
});
670712

671713
await DelayRetry();
672714

@@ -700,7 +742,10 @@ public async Task WaitForReady()
700742
await Task.CompletedTask;
701743
}
702744

703-
protected void UpdateSyncStatus(SyncStatusOptions options)
745+
protected record UpdateSyncStatusOptions(
746+
bool? ClearDownloadError = null, bool? ClearUploadError = null
747+
);
748+
protected void UpdateSyncStatus(SyncStatusOptions options, UpdateSyncStatusOptions? updateOptions = null)
704749
{
705750
var updatedStatus = new SyncStatus(new SyncStatusOptions
706751
{
@@ -710,7 +755,9 @@ protected void UpdateSyncStatus(SyncStatusOptions options)
710755
DataFlow = new SyncDataFlowStatus
711756
{
712757
Uploading = options.DataFlow?.Uploading ?? SyncStatus.DataFlowStatus.Uploading,
713-
Downloading = options.DataFlow?.Downloading ?? SyncStatus.DataFlowStatus.Downloading
758+
Downloading = options.DataFlow?.Downloading ?? SyncStatus.DataFlowStatus.Downloading,
759+
DownloadError = updateOptions?.ClearDownloadError == true ? null : options.DataFlow?.DownloadError ?? SyncStatus.DataFlowStatus.DownloadError,
760+
UploadError = updateOptions?.ClearUploadError == true ? null : options.DataFlow?.UploadError ?? SyncStatus.DataFlowStatus.UploadError,
714761
}
715762
});
716763

PowerSync/PowerSync.Common/DB/Crud/SyncStatus.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ public class SyncDataFlowStatus
99

1010
[JsonProperty("uploading")]
1111
public bool Uploading { get; set; } = false;
12+
13+
/// <summary>
14+
/// Error during downloading (including connecting).
15+
/// Cleared on the next successful data download.
16+
/// </summary>
17+
[JsonProperty("downloadError")]
18+
public Exception? DownloadError { get; set; } = null;
19+
20+
/// <summary>
21+
/// Error during uploading.
22+
/// Cleared on the next successful upload.
23+
/// </summary>
24+
[JsonProperty("uploadError")]
25+
public Exception? UploadError { get; set; } = null;
1226
}
1327

1428
public class SyncStatusOptions
@@ -73,7 +87,7 @@ public bool IsEqual(SyncStatus status)
7387
public string GetMessage()
7488
{
7589
var dataFlow = DataFlowStatus;
76-
return $"SyncStatus<connected: {Connected} connecting: {Connecting} lastSyncedAt: {LastSyncedAt} hasSynced: {HasSynced}. Downloading: {dataFlow.Downloading}. Uploading: {dataFlow.Uploading}>";
90+
return $"SyncStatus<connected: {Connected} connecting: {Connecting} lastSyncedAt: {LastSyncedAt} hasSynced: {HasSynced}. Downloading: {dataFlow.Downloading}. Uploading: {dataFlow.Uploading}. UploadError: {dataFlow.UploadError}, DownloadError?: {dataFlow.DownloadError}>";
7791
}
7892

7993
public string ToJSON()

0 commit comments

Comments
 (0)