-
Notifications
You must be signed in to change notification settings - Fork 11
feat: checkpoint requests #198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5d73115
696d950
4e4f932
093bd8a
8da12bb
0e11137
69bf1d3
12f58aa
a29fe19
2527251
0709764
39045f8
c7254a4
46cc297
caf5370
c86ef3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ use crate::sync::diagnostics::{DiagnosticOptions, DiagnosticsEvent}; | |
| use crate::sync::subscriptions::{StreamKey, apply_subscriptions}; | ||
| use alloc::borrow::Cow; | ||
| use alloc::boxed::Box; | ||
| use alloc::format; | ||
| use alloc::rc::Rc; | ||
| use alloc::{string::String, vec::Vec}; | ||
| use powersync_sqlite_nostd::bindings::SQLITE_RESULT_SUBTYPE; | ||
|
|
@@ -76,6 +77,12 @@ pub enum SyncControlRequest<'a> { | |
| StartSyncStream(StartSyncStream), | ||
| /// The client requests to stop the current sync iteration. | ||
| StopSyncStream, | ||
| /// The client requests a new checkpoint request id. | ||
| NextCheckpointRequestId, | ||
| /// The client probes and optionally updates the local target op. | ||
| /// | ||
| /// This can run outside of a sync iteration and does not affect it. | ||
| ProbeLocalTargetOp { target_op: Option<i64> }, | ||
| /// The client is forwading a sync event to the core extension. | ||
| SyncEvent(SyncEvent<'a>), | ||
| } | ||
|
|
@@ -89,6 +96,10 @@ pub enum SyncEvent<'a> { | |
| /// | ||
| /// In response, we'll stop the current iteration to begin another one with the new token. | ||
| DidRefreshToken, | ||
| /// Seeds the checkpoint request counter from service state. | ||
| SeedCheckpointRequestId { | ||
| request_id: Option<i64>, | ||
| }, | ||
| /// Notifies the sync client that the current CRUD upload (for which the client SDK is | ||
| /// responsible) has finished. | ||
| /// | ||
|
|
@@ -128,13 +139,29 @@ pub enum Instruction { | |
| }, | ||
| /// Connect to the sync service using the [StreamingSyncRequest] created by the core extension, | ||
| /// and then forward received lines via [SyncEvent::TextLine] and [SyncEvent::BinaryLine]. | ||
| EstablishSyncStream { request: StreamingSyncRequest }, | ||
| EstablishSyncStream { | ||
| request: StreamingSyncRequest, | ||
| /// The latest checkpoint request id known locally before opening this stream. | ||
| /// | ||
| /// This is simply the client's current counter state. SDKs use it on every connection | ||
| /// attempt to re-affirm checkpoint request state with the service, which may have deleted | ||
| /// its record. The re-affirmation works bidirectionally: it can restore the service-side | ||
| /// value from this hint or bump the local counter from the service's response, which is | ||
| /// reported back with `seed_checkpoint_request_id`. | ||
| last_checkpoint_request_id: Option<i64>, | ||
| }, | ||
| FetchCredentials { | ||
| /// Whether the credentials currently used have expired. | ||
| /// | ||
| /// If false, this is a pre-fetch. | ||
| did_expire: bool, | ||
| }, | ||
| /// Return a newly allocated checkpoint request id to the SDK. | ||
| CheckpointRequestId { request_id: i64 }, | ||
| /// Notify the SDK that a checkpoint request id has been applied locally. | ||
| CheckpointRequestApplied { request_id: i64 }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I wonder if it makes sense to move that to the sync status as a hidden field only used internally in SDKs. For our SDKs using web workers where it's not that trivial to call methods on the sync client directly, I'm kind of hoping the implementation of (but there are probably a lot more cases I'm missing here) |
||
| /// Return the local target op value observed before an optional update. | ||
| LocalTargetOp { target_op: Option<i64> }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This could use some restructuring still, but overall I think the control flow would be cleaner if we could invoke this directly in "next_checkpoint_request_id" => {
let client = state.sync_client.borrow();
let has_sync_iteration = client
.as_ref()
.map(|e| e.has_sync_iteration())
.unwrap_or(false);
if !has_sync_iteration {
return Err(PowerSyncError::state_error("No iteration is active"));
}
let adapter = state.storage_adapter(db)?;
if !adapter.has_checkpoint_request_id()? {
return Err(PowerSyncError::state_error(
"Checkpoint request state has not been seeded",
));
}
let request_id = adapter.next_checkpoint_request_id()?;
ctx.result_int64(request_id);
return Ok(());
}I don't think there's a need to tick the sync client here, the (I see how my earlier suggestion to move these into |
||
| // These are defined like this because deserializers in Kotlin can't support either an | ||
| // object or a literal value | ||
| /// Close the websocket / HTTP stream to the sync service. | ||
|
|
@@ -232,6 +259,14 @@ pub fn register(db: *mut sqlite::sqlite3, state: Rc<DatabaseState>) -> Result<() | |
| } | ||
| }), | ||
| "stop" => SyncControlRequest::StopSyncStream, | ||
| "next_checkpoint_request_id" => SyncControlRequest::NextCheckpointRequestId, | ||
| "local_target_op" => SyncControlRequest::ProbeLocalTargetOp { | ||
| target_op: parse_optional_i64_payload( | ||
| *payload, | ||
| "local target op", | ||
| "local target op must be an integer, integer string, or null", | ||
| )?, | ||
| }, | ||
| "line_text" => SyncControlRequest::SyncEvent(SyncEvent::TextLine { | ||
| data: if payload.value_type() == ColumnType::Text { | ||
| payload.text() | ||
|
|
@@ -251,6 +286,15 @@ pub fn register(db: *mut sqlite::sqlite3, state: Rc<DatabaseState>) -> Result<() | |
| }, | ||
| }), | ||
| "refreshed_token" => SyncControlRequest::SyncEvent(SyncEvent::DidRefreshToken), | ||
| "seed_checkpoint_request_id" => { | ||
| SyncControlRequest::SyncEvent(SyncEvent::SeedCheckpointRequestId { | ||
| request_id: parse_optional_i64_payload( | ||
| *payload, | ||
| "checkpoint request id", | ||
| "checkpoint request id must be an integer, integer string, or null", | ||
| )?, | ||
| }) | ||
| } | ||
| "completed_upload" => SyncControlRequest::SyncEvent(SyncEvent::UploadFinished), | ||
| "update_subscriptions" => { | ||
| SyncControlRequest::SyncEvent(SyncEvent::DidUpdateSubscriptions { | ||
|
|
@@ -345,3 +389,27 @@ create_sqlite_text_fn!( | |
| powersync_offline_sync_status_impl, | ||
| "powersync_offline_sync_status" | ||
| ); | ||
|
|
||
| fn parse_optional_i64_payload( | ||
| payload: *mut sqlite::value, | ||
| name: &'static str, | ||
| type_error: &'static str, | ||
| ) -> Result<Option<i64>, PowerSyncError> { | ||
| let value = match payload.value_type() { | ||
| ColumnType::Null => return Ok(None), | ||
| ColumnType::Integer => payload.int64(), | ||
| ColumnType::Text => payload | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the motivation for allowing text values, JavaScript compatibility? I'd hope we could handle that with big ints, but if we want this for safety then a small comment might help :) |
||
| .text() | ||
| .parse::<i64>() | ||
| .map_err(|_| PowerSyncError::argument_error(type_error))?, | ||
| _ => return Err(PowerSyncError::argument_error(type_error)), | ||
| }; | ||
|
|
||
| if value < 0 { | ||
| return Err(PowerSyncError::argument_error(format!( | ||
| "{name} must be a non-negative integer" | ||
| ))); | ||
| } | ||
|
|
||
| Ok(Some(value)) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could inline this into
DidCompleteSync?