Skip to content

Commit 5593751

Browse files
seed_checkpoint_request_id should not be a sync event
1 parent d3b39c2 commit 5593751

8 files changed

Lines changed: 75 additions & 60 deletions

File tree

crates/core/src/sync/interface.rs

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,6 @@ pub enum SyncEvent<'a> {
9090
///
9191
/// In response, we'll stop the current iteration to begin another one with the new token.
9292
DidRefreshToken,
93-
/// Seeds the checkpoint request counter from service state.
94-
SeedCheckpointRequestId {
95-
request_id: i64,
96-
},
9793
/// Notifies the sync client that the current CRUD upload (for which the client SDK is
9894
/// responsible) has finished.
9995
///
@@ -249,6 +245,29 @@ pub fn register(db: *mut sqlite::sqlite3, state: Rc<DatabaseState>) -> Result<()
249245
}
250246
}),
251247
"stop" => SyncControlRequest::StopSyncStream,
248+
"seed_checkpoint_request_id" => {
249+
let has_sync_iteration = {
250+
let client = state.sync_client.borrow();
251+
client
252+
.as_ref()
253+
.map(|client| client.has_sync_iteration())
254+
.unwrap_or(false)
255+
};
256+
257+
if !has_sync_iteration {
258+
return Err(PowerSyncError::state_error("No iteration is active"));
259+
}
260+
261+
let request_id = parse_positive_i64_payload(
262+
*payload,
263+
"checkpoint request id",
264+
"checkpoint request id must be an integer or integer string",
265+
)?;
266+
let adapter = state.storage_adapter(db)?;
267+
adapter.seed_checkpoint_request_id(request_id)?;
268+
ctx.result_int64(request_id);
269+
return Ok(());
270+
}
252271
"next_checkpoint_request_id" => {
253272
let has_sync_iteration = {
254273
let client = state.sync_client.borrow();
@@ -316,15 +335,6 @@ pub fn register(db: *mut sqlite::sqlite3, state: Rc<DatabaseState>) -> Result<()
316335
},
317336
}),
318337
"refreshed_token" => SyncControlRequest::SyncEvent(SyncEvent::DidRefreshToken),
319-
"seed_checkpoint_request_id" => {
320-
SyncControlRequest::SyncEvent(SyncEvent::SeedCheckpointRequestId {
321-
request_id: parse_positive_i64_payload(
322-
*payload,
323-
"checkpoint request id",
324-
"checkpoint request id must be an integer or integer string",
325-
)?,
326-
})
327-
}
328338
"completed_upload" => SyncControlRequest::SyncEvent(SyncEvent::UploadFinished),
329339
"update_subscriptions" => {
330340
SyncControlRequest::SyncEvent(SyncEvent::DidUpdateSubscriptions {

crates/core/src/sync/streaming_sync.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -562,10 +562,6 @@ impl StreamingSyncIteration {
562562
.update(|s| s.disconnect(), &mut event.instructions);
563563
break false;
564564
}
565-
SyncEvent::SeedCheckpointRequestId { request_id } => {
566-
self.adapter.seed_checkpoint_request_id(request_id)?;
567-
continue;
568-
}
569565
SyncEvent::TextLine { data } => SyncLineWithSource::from_text(data)?,
570566
SyncEvent::BinaryLine { data } => SyncLineWithSource::from_binary(data)?,
571567
SyncEvent::UploadFinished => {

dart/test/goldens/simple_iteration.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,6 @@
3333
}
3434
]
3535
},
36-
{
37-
"operation": "seed_checkpoint_request_id",
38-
"data": 1,
39-
"output": []
40-
},
4136
{
4237
"operation": "line_text",
4338
"data": {

dart/test/goldens/starting_stream.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,5 @@
3838
}
3939
}
4040
]
41-
},
42-
{
43-
"operation": "seed_checkpoint_request_id",
44-
"data": 1,
45-
"output": []
4641
}
4742
]

dart/test/sync_stream_test.dart

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,7 @@ void main() {
6868
List<Object?> control(String operation, Object? data) {
6969
final result = controlRaw(operation, data);
7070
if (operation == 'start' && establishesSyncStream(result)) {
71-
return [
72-
...result,
73-
...controlRaw('seed_checkpoint_request_id', 1),
74-
];
71+
controlRaw('seed_checkpoint_request_id', 1);
7572
}
7673
return result;
7774
}

dart/test/sync_test.dart

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,12 @@ void _syncTests<T>({
6060

6161
db.execute('commit');
6262
final [row] = result;
63-
return jsonDecode(row.columnAt(0));
63+
final rawResult = row.columnAt(0);
64+
if (rawResult is String) {
65+
return jsonDecode(rawResult);
66+
} else {
67+
return const [];
68+
}
6469
}
6570

6671
Object? invokeControlScalar(String operation, Object? data) {
@@ -83,6 +88,10 @@ void _syncTests<T>({
8388
return row.columnAt(0);
8489
}
8590

91+
int seedCheckpointRequestId(Object? requestId) {
92+
return invokeControlScalar('seed_checkpoint_request_id', requestId) as int;
93+
}
94+
8695
bool establishesSyncStream(List<Object?> instructions) {
8796
return instructions.any((instruction) =>
8897
instruction is Map && instruction.containsKey('EstablishSyncStream'));
@@ -98,10 +107,7 @@ void _syncTests<T>({
98107
}
99108

100109
if (operation == 'start' && establishesSyncStream(result)) {
101-
final seedResult = matcher.enabled
102-
? matcher.invoke('seed_checkpoint_request_id', 1)
103-
: invokeControlRaw('seed_checkpoint_request_id', 1);
104-
return [...result, ...seedResult];
110+
seedCheckpointRequestId(1);
105111
}
106112

107113
return result;
@@ -246,15 +252,12 @@ void _syncTests<T>({
246252
});
247253

248254
syncTest('app_metadata is passed to EstablishSyncStream request', (_) {
249-
final startInstructions = [
250-
...invokeControlRaw(
251-
'start',
252-
json.encode({
253-
'app_metadata': {'key1': 'value1', 'key2': 'value2'}
254-
}),
255-
),
256-
...invokeControlRaw('seed_checkpoint_request_id', 1),
257-
];
255+
final startInstructions = invokeControlRaw(
256+
'start',
257+
json.encode({
258+
'app_metadata': {'key1': 'value1', 'key2': 'value2'}
259+
}),
260+
);
258261

259262
expect(
260263
startInstructions,
@@ -470,7 +473,7 @@ void _syncTests<T>({
470473
expect(currentCheckpointRequestId(), isNull);
471474

472475
invokeControlRaw('start', null);
473-
invokeControlRaw('seed_checkpoint_request_id', 1);
476+
expect(seedCheckpointRequestId(1), 1);
474477
expect(currentCheckpointRequestId(), 1);
475478

476479
expect(currentCheckpointRequestId(), 1);
@@ -494,7 +497,7 @@ void _syncTests<T>({
494497
syncTest('seeds requested checkpoint request ids from service state', (_) {
495498
final startInstructions = invokeControlRaw('start', null);
496499
expect(streamLastCheckpointRequestId(startInstructions), isNull);
497-
invokeControlRaw('seed_checkpoint_request_id', 41);
500+
expect(seedCheckpointRequestId(41), 41);
498501

499502
expect(nextCheckpointRequestId(), 42);
500503
expect(lastRequestedCheckpointRequestId(), 42);
@@ -505,21 +508,21 @@ void _syncTests<T>({
505508
contains(containsPair('EstablishSyncStream', anything)),
506509
);
507510
expect(streamLastCheckpointRequestId(restartInstructions), 42);
508-
expect(invokeControlRaw('seed_checkpoint_request_id', 100), isEmpty);
511+
expect(seedCheckpointRequestId(100), 100);
509512

510513
expect(nextCheckpointRequestId(), 101);
511514
expect(lastRequestedCheckpointRequestId(), 101);
512515
});
513516

514517
syncTest('stores seeded checkpoint request ids verbatim', (_) {
515518
invokeControlRaw('start', null);
516-
invokeControlRaw('seed_checkpoint_request_id', 41);
519+
expect(seedCheckpointRequestId(41), 41);
517520
expect(lastRequestedCheckpointRequestId(), 41);
518521

519522
// Core does not enforce monotonicity when seeding. SDKs own reconciliation and seed the
520523
// effective state accepted by the service, which may be below the local counter (e.g. after
521524
// switching users).
522-
invokeControlRaw('seed_checkpoint_request_id', 5);
525+
expect(seedCheckpointRequestId(5), 5);
523526
expect(lastRequestedCheckpointRequestId(), 5);
524527
expect(nextCheckpointRequestId(), 6);
525528
});
@@ -552,12 +555,23 @@ void _syncTests<T>({
552555

553556
syncTest('accepts text checkpoint request ids when seeding', (_) {
554557
invokeControlRaw('start', null);
555-
invokeControlRaw('seed_checkpoint_request_id', '41');
558+
expect(seedCheckpointRequestId('41'), 41);
556559

557560
expect(lastRequestedCheckpointRequestId(), 41);
558561
expect(nextCheckpointRequestId(), 42);
559562
});
560563

564+
syncTest('requires active sync iteration before seeding checkpoint ids', (_) {
565+
expect(
566+
() => seedCheckpointRequestId(1),
567+
throwsA(isSqliteException(
568+
21,
569+
contains('No iteration is active'),
570+
)),
571+
);
572+
expect(lastRequestedCheckpointRequestId(), isNull);
573+
});
574+
561575
syncTest('requires checkpoint request state before allocating checkpoint ids',
562576
(_) {
563577
invokeControlRaw('start', null);
@@ -572,7 +586,9 @@ void _syncTests<T>({
572586
expect(probeTargetCheckpointRequestId(), isNull);
573587
});
574588

575-
syncTest('probes and updates target checkpoint request id without sync iteration', (_) {
589+
syncTest(
590+
'probes and updates target checkpoint request id without sync iteration',
591+
(_) {
576592
expect(probeTargetCheckpointRequestId(), isNull);
577593
expect(probeTargetCheckpointRequestId(1), isNull);
578594
expect(lastRequestedCheckpointRequestId(), isNull);
@@ -583,7 +599,9 @@ void _syncTests<T>({
583599
expect(probeTargetCheckpointRequestId(), 2);
584600
});
585601

586-
syncTest('accepts text checkpoint request ids for target checkpoint request id', (_) {
602+
syncTest(
603+
'accepts text checkpoint request ids for target checkpoint request id',
604+
(_) {
587605
expect(probeTargetCheckpointRequestId('1'), isNull);
588606
expect(lastRequestedCheckpointRequestId(), isNull);
589607
expect(probeTargetCheckpointRequestId(), 1);
@@ -599,9 +617,10 @@ void _syncTests<T>({
599617
);
600618
});
601619

602-
syncTest('target checkpoint request id does not update checkpoint request id', (_) {
620+
syncTest('target checkpoint request id does not update checkpoint request id',
621+
(_) {
603622
invokeControlRaw('start', null);
604-
invokeControlRaw('seed_checkpoint_request_id', 10);
623+
expect(seedCheckpointRequestId(10), 10);
605624

606625
expect(lastRequestedCheckpointRequestId(), 10);
607626
expect(probeTargetCheckpointRequestId(7), isNull);

docs/sync.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ The following commands are supported:
4545
clears the target checkpoint request id and returns the previously-observed value as an integer result, or
4646
SQL `NULL` if there was no target. This command can run outside of a sync iteration and does not
4747
affect it.
48-
13. `seed_checkpoint_request_id`: Payload is a positive integer or integer string. After receiving
49-
`EstablishSyncStream`, SDKs should reconcile the local hint with service-side
50-
checkpoint-request state, then seed core with the accepted positive id.
48+
13. `seed_checkpoint_request_id`: Payload is a positive integer or integer string. During an active
49+
sync iteration, after receiving `EstablishSyncStream`, SDKs should reconcile the local hint with
50+
service-side checkpoint-request state, then seed core with the accepted positive id. Returns the
51+
seeded id as an integer result.
5152

5253
## Checkpoint Request Expectations
5354

@@ -57,8 +58,8 @@ checkpoint waiters (through `DidCompleteSync.applied_checkpoint_request_id` or t
5758
sync-status field) are documented in `write-checkpoint-requests.md`.
5859

5960
Most `powersync_control` commands return a JSON-encoded array of instructions for the client.
60-
`next_checkpoint_request_id`, `current_checkpoint_request_id` and `target_checkpoint_request_id` return values
61-
directly.
61+
`seed_checkpoint_request_id`, `next_checkpoint_request_id`, `current_checkpoint_request_id` and
62+
`target_checkpoint_request_id` return values directly.
6263

6364
```typescript
6465
type Instruction = { LogLine: LogLine }

docs/write-checkpoint-requests.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ checkpoint state.
146146
`powersync_control('seed_checkpoint_request_id', id)`
147147

148148
- Payload: positive integer or integer string.
149+
- Returns: seeded checkpoint request id as a SQLite integer.
149150
- Stores the reconciled checkpoint-request counter seed.
151+
- Requires an active sync iteration.
150152
- Must be called after connection reconciliation and before allocating new ids.
151153

152154
`powersync_control('next_checkpoint_request_id', NULL)`

0 commit comments

Comments
 (0)