Skip to content
Open
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
205 changes: 192 additions & 13 deletions client-sdks/advanced/checkpoint-requests.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ PowerSync syncs continuously in the background and [SyncStatus](/client-sdks/usa
Checkpoint requests let you catch up on demand: you request a marker of the current server state, then wait until the local database has caught up to it. Waiting covers uploads as well as downloads: a request created after local writes confirms that those writes have been uploaded and their results have synced back (see [Relationship to Local Writes](#relationship-to-local-writes)).

<Warning>
Checkpoint requests are an alpha API and may change. Client support is currently available for Swift and requires PowerSync Service version 1.24.0 or later. Support for the JavaScript SDKs is [in progress](https://github.com/powersync-ja/powersync-js/pull/1072), and other SDKs will follow.
Checkpoint requests are an alpha API and may change. Support requires PowerSync Service version 1.24.0 or later. Checkpoint requests are not currently available for the .NET and Rust SDKs.
</Warning>

Example use cases include:
Expand Down Expand Up @@ -37,16 +37,37 @@ See [Consistency](/architecture/consistency) for more information about how Powe

Before creating a checkpoint request:

1. Run PowerSync Swift SDK v1.16.0 or later.
2. Run PowerSync Service v1.24.0 or later.
3. Connect with `checkpointMode` set to `.requests()`.
1. Run PowerSync Service v1.24.0 or later.
2. Opt in to checkpoint requests with the `checkpointMode` option when calling `connect()`.

```swift
<CodeGroup>
```typescript JavaScript/TypeScript
await db.connect(connector, { checkpointMode: 'requests' });
```

```dart Dart
await db.connect(
connector: connector,
options: SyncOptions(checkpointMode: .requests()),
Comment thread
simolus3 marked this conversation as resolved.
);
```

```kotlin Kotlin
database.connect(
connector,
options = SyncOptions(
checkpointMode = CheckpointMode.Requests(),
)
)
```

```swift Swift
try await database.connect(
connector: connector,
options: ConnectOptions(checkpointMode: .requests())
)
```
</CodeGroup>

<Note>
Checkpoint requests are currently opt-in while this feature is in alpha. Without this connect option, calling `requestCheckpoint()` throws an error. Checkpoint requests will be enabled by default in a future release.
Expand All @@ -56,25 +77,96 @@ try await database.connect(

Create a checkpoint request, then wait for it to sync before reading the refreshed data:

```swift
<CodeGroup>
```typescript JavaScript/TypeScript
async function refreshLocalData() {
const checkpoint = await database.requestCheckpoint();
await checkpoint.waitForSync({ signal: AbortSignal.timeout(30_000) });
}
```

```dart Dart
Future<void> refreshLocalData() async {
final checkpoint = await database.requestCheckpoint();
await checkpoint.waitForSync(
abortTrigger: Future.pause(Duration(seconds: 30))
Comment thread
simolus3 marked this conversation as resolved.
);
}
```

```kotlin Kotlin
suspend fun refreshLocalData() {
val checkpoint = database.requestCheckpoint()
withTimeout(30.seconds) {
checkpoint.waitForSync()
}
}
```

```swift Swift
func refreshLocalData() async throws {
let checkpoint = try await database.requestCheckpoint()
try await checkpoint.waitForSync(timeout: 30)
// Local queries now reflect server state from when the request was made.
}
```
</CodeGroup>

`requestCheckpoint()` requires that the database is connected or connecting. The device must be online for the request to reach the PowerSync Service. If it is offline or the sync client is reconnecting, the call waits and continues once the Service is reachable.

Creating the request has no timeout of its own, so it can stay suspended while the sync client retries its connection. Cancel the calling task if you need to stop waiting.

The timeout passed to `waitForSync(timeout:)` only limits how long you wait for the checkpoint to sync and apply locally.
Aborting a `waitForSync` call or configuring a timeout does not remove the checkpoint. It only limits how long you wait for the checkpoint to sync and apply locally.

## Handling Wait Failures

Handle request creation and waiting errors separately when your app needs different recovery behavior:

```swift
<CodeGroup>
```typescript JavaScript/TypeScript
const signal = AbortSignal.timeout(30_000);

try {
const checkpoint = await database.requestCheckpoint();
await checkpoint.waitForSync({ signal });
} catch (e) {
if (signal.aborted) {
showRefreshMessage('The refresh timed out. Try again.');
} else {
showRefreshMessage(`Could not wait for checkpoint: ${e}`);
}
}
```

```dart Dart
try {
final checkpoint = await database.requestCheckpoint();
await checkpoint.waitForSync(
abortTrigger: Future.pause(Duration(seconds: 30))
);
} on AbortException {
showRefreshMessage('The refresh timed out. Try again.');
} catch (e) {
showRefreshMessage('Could not wait for checkpoint: $e');
}
```

```kotlin Kotlin
try {
val checkpoint = database.requestCheckpoint()
withTimeout(30.seconds) {
checkpoint.waitForSync()
}
} catch (e: TimeoutCancellationException) {
showRefreshMessage("The refresh timed out. Try again.")
} catch (e: CheckpointRequestException.Disconnected) {
showRefreshMessage("Reconnect before refreshing again.")
} catch (e: Exception) {
// Other checkpoint exceptions
}
```

```swift Swift
do {
let checkpoint = try await database.requestCheckpoint()
try await checkpoint.waitForSync(timeout: 30)
Expand All @@ -86,20 +178,46 @@ do {
showRefreshMessage(error.localizedDescription)
}
```
</CodeGroup>

A request remains valid across a disconnect. After reconnecting with `.requests()`, you can call `waitForSync()` again on the same request. Discard existing request values after clearing the local PowerSync database because clearing it resets the persisted request state.

`waitForSync()` also fails if the sync client reports an upload or download error. Wait for sync to recover before retrying.

## Relationship to Local Writes

PowerSync never applies a checkpoint while local writes are waiting to upload, so sync cannot revert your own pending changes. When `.requests()` mode is enabled, the PowerSync Client SDK maintains this guarantee with checkpoint requests: each time it finishes uploading the local write queue, it internally creates a request that captures a source position from after the upload completed. You do not need to call `requestCheckpoint()` for your own writes.
PowerSync never applies a checkpoint while local writes are waiting to upload, so sync cannot revert your own pending changes. When checkpoint requests are enabled, the PowerSync Client SDK maintains this guarantee with checkpoint requests: each time it finishes uploading the local write queue, it internally creates a request that captures a source position from after the upload completed. You do not need to call `requestCheckpoint()` for your own writes.

`waitForSync()` considers a request complete when the same or a newer checkpoint request has been applied locally. This makes explicit requests safe to combine with pending writes. If you create a request while local writes are waiting to upload, it is not applied while they are pending; once the upload queue empties, the SDK's newer internal request supersedes it and captures a source position from after the upload. If you create the request after the SDK's internal request instead, it captures an even later position.

In both cases, waiting on the request also waits for the pending upload and for its result to sync back. You can therefore write locally and wait for the uploaded result to return through sync:

```swift
<CodeGroup>
```typescript JavaScript/TypeScript
await database.execute('INSERT INTO tasks (id, description) VALUES (uuid(), ?)', ['Review the project plan']);
const checkpoint = await database.requestCheckpoint();
await checkpoint.waitForSync();
```

```dart Dart
await database.execute(
'INSERT INTO tasks (id, description) VALUES (uuid(), ?)',
['Review the project plan'],
);
final checkpoint = await database.requestCheckpoint();
await checkpoint.waitForSync();
```

```kotlin Kotlin
database.execute(
"INSERT INTO tasks (id, description) VALUES (uuid(), ?)",
listOf("Review the project plan")
)
val checkpoint = database.requestCheckpoint()
checkpoint.waitForSync()
```

```swift Swift
try await database.execute(
sql: "INSERT INTO tasks (id, description) VALUES (uuid(), ?)",
parameters: ["Review the project plan"]
Expand All @@ -109,6 +227,7 @@ let checkpoint = try await database.requestCheckpoint()
try await checkpoint.waitForSync(timeout: 30)
// The pending write has uploaded and its source state has synced locally.
```
</CodeGroup>

This behavior relies on `uploadData()` returning only after your backend has committed the uploaded changes to the source database. See [Writing Client Changes](/handling-writes/writing-client-changes#why-must-my-write-endpoint-be-synchronous) for the reason your write endpoint must be synchronous.

Expand Down Expand Up @@ -136,11 +255,65 @@ This comparison makes repeated and stale requests idempotent. It also handles se

You can delete stored request records after an appropriate retention period. While a record exists, return its value during reconciliation so the SDK can resume from that value.

### Swift Connector
### Connector Changes

To make the PowerSync client SDK post checkpoint requests to your backend instead of to the PowerSync Service,
declare support for checkpoint requests on your backend connector:

- For JavaScript, implement the optional `postCheckpointRequest` method.
- For Dart, mix in the `CustomCheckpointRequestConnector` class.
- For Kotlin, implement `CustomCheckpointRequestConnector`.
- For Swift, make your connector conform to `CustomCheckpointRequestConnector`.

Forward the request to your application backend:

Make your existing connector conform to `CustomCheckpointRequestConnector` and forward the request to your application backend:
<CodeGroup>
```typescript JavaScript/TypeScript
class MyBackendConnector implements PowerSyncBackendConnector {
// ... also implement fetchCredentials and uploadData

```swift
async postCheckpointRequest(clientId: string, requestId: string): Promise<string> {
const response = await MyBackendConnector.createCheckpointRequest(clientId, requestId);
return response.checkpointRequestId;
}
}
```

```dart Dart
final class MyBackendConnector extends PowerSyncBackendConnector
with CustomCheckpointRequestConnector {
// ... also implement fetchCredentials and uploadData

@override
Future<String> postCheckpointRequest(
String clientId,
String requestId,
) async {
final response = await myBackend.createCheckpointRequest(
clientId,
requestId,
);
return response.checkpointRequestId;
}
}
```

```kotlin Kotlin
class MyBackendConnector: PowerSyncBackendConnector(), CustomCheckpointRequestConnector {
override suspend fun fetchCredentials(): PowerSyncCredentials? = TODO()
override suspend fun uploadData(database: PowerSyncDatabase) = TODO()

override suspend fun postCheckpointRequest(
clientId: String,
requestId: Long
): Long {
val response = myBackend.createCheckpointRequest(clientId, requestId)
return response.checkpointRequestId
}
}
```

```swift Swift
extension BackendConnector: CustomCheckpointRequestConnector {
func postCheckpointRequest(
_ checkpointRequestId: Int64,
Expand All @@ -155,5 +328,11 @@ extension BackendConnector: CustomCheckpointRequestConnector {
}
}
```
</CodeGroup>

The connector must use your application's own authentication for this call because `postCheckpointRequest()` does not receive the PowerSync sync token.

<Tip>
Checkpoint IDs are positive integers between 1 and 2<sup>63</sup> - 1 (inclusive). For compatibility with JavaScript numbers,
the JavaScript and Dart client SDKs represent requests as strings.
</Tip>
Loading