Skip to content
Merged
Show file tree
Hide file tree
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
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/cpp/callback_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ void sync_event_callback(
// Error callback - receives error message
void error_event_callback(
EventType event_type,
int32_t error_code,
const char* error,
void* context)
{
Expand Down
2 changes: 1 addition & 1 deletion replicant-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "replicant-client"
version = "0.3.0"
version = "0.4.0"
edition = "2021"

[dependencies]
Expand Down
1 change: 1 addition & 0 deletions replicant-client/cbindgen.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ include = [
"SyncResult",
"Document",
"EventType",
"ReplicantErrorCode",
"DocumentEventCallback",
"SyncEventCallback",
"ErrorEventCallback",
Expand Down
2 changes: 1 addition & 1 deletion replicant-client/examples/cpp_lambda_callbacks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ void on_connection_event(EventType event_type, bool is_connected,
/**
* @brief Error event callback
*/
void on_error_event(EventType event_type, const char* error_message, void* context)
void on_error_event(EventType event_type, int32_t error_code, const char* error_message, void* context)
{
auto* stats = static_cast<EventStats*>(context);
stats->errors++;
Expand Down
2 changes: 1 addition & 1 deletion replicant-client/examples/task_list_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
app_state.last_sync = Some(Instant::now());
app_state.needs_refresh = true;
}
SyncEvent::SyncError { message } => {
SyncEvent::SyncError { message, .. } => {
app_state.add_activity(
format!("Sync error: {}", message),
ActivityType::Error,
Expand Down
2 changes: 1 addition & 1 deletion replicant-client/examples/test_rust_callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
SyncEvent::SyncCompleted { document_count } => {
format!("✅ Sync completed: {} docs", document_count)
}
SyncEvent::SyncError { message } => {
SyncEvent::SyncError { message, .. } => {
format!("🚨 Sync error: {}", message)
}
SyncEvent::ConnectionLost { server_url } => {
Expand Down
94 changes: 93 additions & 1 deletion replicant-client/include/replicant.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,81 @@ typedef enum ReplicantEventType {
IdentityChanged = 10,
} ReplicantEventType;

/**
* Structured error code carried by every `SyncError` event.
*
* The numeric values are STABLE and exported to C via cbindgen. They are
* banded by the action a consumer should take:
*
* - `0` — unknown / uncategorized.
* - `1xxx` — **credential rejected**: the stored credential is bad. The
* consumer should clear it and re-enroll. See [`is_credential_rejection`].
* - `2xxx` — **transient**: retry later; NEVER clear credentials. This band
* includes the timestamp reasons (`2101`, `2102`), which are client/server
* clock skew — not a bad credential — and so must never trigger a clear.
* - `3xxx` — **protocol**: the exchange was malformed or violated the contract.
* - `4xxx` — **identity drift**: the local identity diverged from the account;
* refuse to sync, but do NOT clear credentials.
*/
enum ReplicantErrorCode
#ifdef __cplusplus
: int32_t
#endif // __cplusplus
{
/**
* Unknown or uncategorized error.
*/
Unknown = 0,
/**
* The API key is not recognized by the server.
*/
InvalidApiKey = 1001,
/**
* The HMAC signature did not verify.
*/
InvalidSignature = 1002,
/**
* The credential authenticated but is not bound to an enrolled user.
*/
CredentialNotEnrolled = 1003,
/**
* The socket/transport failed to connect.
*/
ConnectionFailed = 2001,
/**
* A join or call timed out.
*/
Timeout = 2002,
/**
* The signed timestamp was outside the server's acceptance window
* (client/server clock skew, not a bad credential).
*/
TimestampExpired = 2101,
/**
* The timestamp field was malformed or unparseable (treated as clock skew).
*/
InvalidTimestamp = 2102,
/**
* A required join parameter was missing.
*/
MissingParams = 3001,
/**
* The join topic's user id did not match the credential's user.
*/
TopicUserMismatch = 3002,
/**
* Generic malformed or unexpected server reply.
*/
ProtocolError = 3003,
/**
* The server-reported user id diverged from the local identity.
*/
IdentityDrift = 4001,
};
#ifndef __cplusplus
typedef int32_t ReplicantErrorCode;
#endif // __cplusplus

/**
* Result codes for C API functions
*/
Expand Down Expand Up @@ -124,10 +199,14 @@ typedef void (*SyncEventCallback)(enum ReplicantEventType event_type,
*
* # Parameters
* * `event_type` - Always SyncError
* * `error_code` - Stable `ReplicantErrorCode` value; use
* `replicant_error_is_credential_rejection` to decide whether to clear the
* stored credential
* * `error` - Error message (always non-null)
* * `context` - User-defined context pointer
*/
typedef void (*ErrorEventCallback)(enum ReplicantEventType event_type,
int32_t error_code,
const char *error,
void *context);

Expand Down Expand Up @@ -191,6 +270,18 @@ typedef struct Document {
extern "C" {
#endif // __cplusplus

/**
* Band check exposed over FFI: `true` iff `code` is a credential rejection.
*
* Bindings should treat a `true` result as "clear the stored credential and
* re-enroll". Implemented once here so consumers do not re-implement the band
* logic against the raw numeric values.
*
* # Safety
* This function is pure and takes the code by value; it is always safe to call.
*/
bool replicant_error_is_credential_rejection(int32_t code);

/**
* Create a new sync engine instance
*
Expand Down Expand Up @@ -613,7 +704,8 @@ enum ReplicantSyncResult replicant_rebuild_search_index(struct Replicant *engine

/**
* Requests an enrollment token be emailed to `email`. Standalone HTTP call
* (no engine handle); spins a short-lived runtime to drive the async request.
* (no engine handle); runs on a dedicated thread with its own short-lived
* runtime so this is safe to call even from inside an async runtime context.
*
* # Safety
* `base_url` and `email` must be valid, non-null C strings.
Expand Down
29 changes: 16 additions & 13 deletions replicant-client/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use crate::{database::ClientDatabase, events::EventDispatcher, websocket::WebSocketClient};
use crate::{
database::ClientDatabase, error_code::ReplicantErrorCode, events::EventDispatcher,
websocket::WebSocketClient,
};
use replicant_core::{
errors::ClientError,
models::{Document, SyncStatus},
Expand Down Expand Up @@ -1364,10 +1367,10 @@ impl Client {
error.as_deref().unwrap_or("unknown error")
);
// Could emit an error event here
event_dispatcher.emit_sync_error(&format!(
"Create failed: {}",
error.as_deref().unwrap_or("unknown")
));
event_dispatcher.emit_sync_error(
ReplicantErrorCode::Unknown,
&format!("Create failed: {}", error.as_deref().unwrap_or("unknown")),
);
}
}

Expand Down Expand Up @@ -1408,10 +1411,10 @@ impl Client {
document_id,
error.as_deref().unwrap_or("unknown error")
);
event_dispatcher.emit_sync_error(&format!(
"Update failed: {}",
error.as_deref().unwrap_or("unknown")
));
event_dispatcher.emit_sync_error(
ReplicantErrorCode::Unknown,
&format!("Update failed: {}", error.as_deref().unwrap_or("unknown")),
);
}
}

Expand All @@ -1436,10 +1439,10 @@ impl Client {
document_id,
error.as_deref().unwrap_or("unknown error")
);
event_dispatcher.emit_sync_error(&format!(
"Delete failed: {}",
error.as_deref().unwrap_or("unknown")
));
event_dispatcher.emit_sync_error(
ReplicantErrorCode::Unknown,
&format!("Delete failed: {}", error.as_deref().unwrap_or("unknown")),
);
}
}

Expand Down
Loading
Loading