Skip to content

Commit d40c8da

Browse files
add internal_applied_checkpoint_request_id to sync status
1 parent de130f4 commit d40c8da

6 files changed

Lines changed: 69 additions & 5 deletions

File tree

crates/core/src/sync/storage_adapter.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ impl StorageAdapter {
133133
priority_status: priority_items,
134134
downloading: None,
135135
streams,
136+
// Checkpoint requests should not be made or compared while offline.
137+
internal_applied_checkpoint_request_id: None,
136138
})
137139
}
138140

crates/core/src/sync/streaming_sync.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -972,7 +972,7 @@ impl StreamingSyncIteration {
972972
});
973973

974974
self.status.update(
975-
|status| status.applied_checkpoint(timestamp),
975+
|status| status.applied_checkpoint(timestamp, applied_checkpoint_request_id),
976976
&mut event.instructions,
977977
);
978978
}

crates/core/src/sync/sync_status.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ pub struct DownloadSyncStatus {
5353
/// received), information about how far the download has progressed.
5454
pub downloading: Option<SyncDownloadProgress>,
5555
pub streams: Vec<ActiveStreamSubscription>,
56+
/// Runtime-only request id from the most recent full checkpoint apply.
57+
///
58+
/// This is exposed in sync status for SDK internals, but it is not persisted and should not be
59+
/// treated as user-facing download progress.
60+
pub internal_applied_checkpoint_request_id: Option<i64>,
5661
}
5762

5863
impl DownloadSyncStatus {
@@ -73,6 +78,7 @@ impl DownloadSyncStatus {
7378
self.connected = false;
7479
self.downloading = None;
7580
self.connecting = true;
81+
self.internal_applied_checkpoint_request_id = None;
7682
self.debug_assert_priority_status_is_sorted();
7783
}
7884

@@ -117,7 +123,11 @@ impl DownloadSyncStatus {
117123
self.debug_assert_priority_status_is_sorted();
118124
}
119125

120-
pub fn applied_checkpoint(&mut self, now: TimestampMicros) {
126+
pub fn applied_checkpoint(
127+
&mut self,
128+
now: TimestampMicros,
129+
applied_checkpoint_request_id: Option<i64>,
130+
) {
121131
self.downloading = None;
122132
self.priority_status.clear();
123133

@@ -126,6 +136,8 @@ impl DownloadSyncStatus {
126136
last_synced_at: Some(now),
127137
has_synced: Some(true),
128138
});
139+
140+
self.internal_applied_checkpoint_request_id = applied_checkpoint_request_id;
129141
}
130142
}
131143

@@ -137,6 +149,7 @@ impl Default for DownloadSyncStatus {
137149
downloading: None,
138150
priority_status: Vec::new(),
139151
streams: Vec::new(),
152+
internal_applied_checkpoint_request_id: None,
140153
}
141154
}
142155
}
@@ -180,12 +193,16 @@ impl Serialize for DownloadSyncStatus {
180193
}
181194
}
182195

183-
let mut serializer = serializer.serialize_struct("DownloadSyncStatus", 5)?;
196+
let field_count = 5 + usize::from(self.internal_applied_checkpoint_request_id.is_some());
197+
let mut serializer = serializer.serialize_struct("DownloadSyncStatus", field_count)?;
184198
serializer.serialize_field("connected", &self.connected)?;
185199
serializer.serialize_field("connecting", &self.connecting)?;
186200
serializer.serialize_field("priority_status", &self.priority_status)?;
187201
serializer.serialize_field("downloading", &self.downloading)?;
188202
serializer.serialize_field("streams", &SerializeStreamsWithProgress(self))?;
203+
if let Some(request_id) = self.internal_applied_checkpoint_request_id {
204+
serializer.serialize_field("internal_applied_checkpoint_request_id", &request_id)?;
205+
}
189206

190207
serializer.end()
191208
}

dart/test/sync_test.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,15 @@ void _syncTests<T>({
400400
invokeControl('stop', null);
401401

402402
final instructions = invokeControl('start', null);
403+
expect(
404+
instructions,
405+
isNot(contains(containsPair(
406+
'UpdateSyncStatus',
407+
containsPair(
408+
'status',
409+
containsPair(
410+
'internal_applied_checkpoint_request_id', anything))))),
411+
);
403412
expect(
404413
instructions,
405414
contains(
@@ -609,6 +618,15 @@ void _syncTests<T>({
609618
isNot(contains(containsPair('DidCompleteSync',
610619
containsPair('applied_checkpoint_request_id', anything)))),
611620
);
621+
expect(
622+
instructions,
623+
isNot(contains(containsPair(
624+
'UpdateSyncStatus',
625+
containsPair(
626+
'status',
627+
containsPair(
628+
'internal_applied_checkpoint_request_id', anything))))),
629+
);
612630
expect(lastAppliedCheckpointRequestId(), isNull);
613631

614632
final [row] = db.select('select powersync_offline_sync_status();');
@@ -634,6 +652,16 @@ void _syncTests<T>({
634652
),
635653
),
636654
);
655+
expect(
656+
appliedInstructions,
657+
contains(containsPair(
658+
'UpdateSyncStatus',
659+
containsPair(
660+
'status',
661+
containsPair('internal_applied_checkpoint_request_id', 1),
662+
),
663+
)),
664+
);
637665
expect(lastAppliedCheckpointRequestId(), 1);
638666

639667
pushCheckpoint(buckets: priorityBuckets);
@@ -643,6 +671,15 @@ void _syncTests<T>({
643671
instructions,
644672
contains(containsPair('DidCompleteSync', <String, Object?>{})),
645673
);
674+
expect(
675+
instructions,
676+
isNot(contains(containsPair(
677+
'UpdateSyncStatus',
678+
containsPair(
679+
'status',
680+
containsPair(
681+
'internal_applied_checkpoint_request_id', anything))))),
682+
);
646683

647684
final [row] = db.select('select powersync_offline_sync_status();');
648685
expect(

docs/sync.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ what SDKs need to do.
6262
id to the service, then store the accepted id with `powersync_control('local_target_op', id)`.
6363
- `local_target_op` is the apply gate for local writes. `next_checkpoint_request_id` only allocates
6464
ids; it does not update that gate.
65-
- Resolve explicit checkpoint waiters from `DidCompleteSync.applied_checkpoint_request_id`, not from
66-
`ps_kv`.
65+
- Resolve explicit checkpoint waiters from `DidCompleteSync.applied_checkpoint_request_id`. SDKs
66+
that drive waiters from status snapshots can also watch
67+
`UpdateSyncStatus.status.internal_applied_checkpoint_request_id`. Treat that status field as
68+
runtime-only SDK state, not persisted checkpoint state or app-visible progress.
6769

6870
Most `powersync_control` commands return a JSON-encoded array of instructions for the client.
6971
`next_checkpoint_request_id` and `local_target_op` return scalar values directly.
@@ -105,6 +107,7 @@ interface UpdateSyncStatus {
105107
priority_status: [],
106108
downloading: null | DownloadProgress,
107109
streams: [],
110+
internal_applied_checkpoint_request_id?: number,
108111
}
109112

110113
interface DidCompleteSync {

docs/write-checkpoint-requests.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ DidCompleteSync { applied_checkpoint_request_id?: checkpoint.write_checkpoint }
121121
SDKs should resolve `requestCheckpoint()` / `waitForSync()` waiters when this value is greater than
122122
or equal to the requested id.
123123

124+
Core also includes the same applied request id in
125+
`UpdateSyncStatus.status.internal_applied_checkpoint_request_id` for the status update emitted with
126+
the completed checkpoint. SDKs may use that for status-stream waiters, but should treat it as
127+
internal, runtime-only state rather than app-visible progress or persisted checkpoint state.
128+
124129
## Control Commands
125130

126131
`powersync_control('seed_checkpoint_request_id', id)`

0 commit comments

Comments
 (0)