diff --git a/Makefile b/Makefile index 0820b4a..c5970df 100644 --- a/Makefile +++ b/Makefile @@ -136,6 +136,16 @@ endif T_LDFLAGS += -fprofile-arcs -ftest-coverage endif +# Native network support only for Apple platforms +ifdef NATIVE_NETWORK + RELEASE_OBJ += $(patsubst %.m, $(BUILD_RELEASE)/%_m.o, $(notdir $(wildcard $(SRC_DIR)/*.m))) + LDFLAGS += -framework Foundation + CFLAGS += -DCLOUDSYNC_OMIT_CURL + +$(BUILD_RELEASE)/%_m.o: %.m + $(CC) $(CFLAGS) -fobjc-arc -O3 -fPIC -c $< -o $@ +endif + # Windows .def file generation $(DEF_FILE): ifeq ($(PLATFORM),windows) @@ -314,15 +324,15 @@ clean: help: @echo "SQLite Sync Extension Makefile" @echo "Usage:" - @echo " make [PLATFORM=platform] [ARCH=arch] [ANDROID_NDK=\$$ANDROID_HOME/ndk/26.1.10909125] [target]" + @echo " make [PLATFORM=platform] [ARCH=arch] [ANDROID_NDK=\$$ANDROID_HOME/ndk/26.1.10909125] [NATIVE_NETWORK=ON] [target]" @echo "" @echo "Platforms:" @echo " linux (default on Linux)" - @echo " macos (default on macOS)" + @echo " macos (default on macOS - can be compiled with native network support)" @echo " windows (default on Windows)" @echo " android (needs ARCH to be set to x86_64 or arm64-v8a and ANDROID_NDK to be set)" - @echo " ios (only on macOS)" - @echo " isim (only on macOS)" + @echo " ios (only on macOS - can be compiled with native network support)" + @echo " isim (only on macOS - can be compiled with native network support)" @echo " wasm (needs wabt[brew install wabt/sudo apt install wabt])" @echo "" @echo "Targets:" diff --git a/examples/sport-tracker-app/package.json b/examples/sport-tracker-app/package.json index 5897d20..f8bc85d 100644 --- a/examples/sport-tracker-app/package.json +++ b/examples/sport-tracker-app/package.json @@ -13,7 +13,7 @@ "vite": "^7.0.0" }, "dependencies": { - "@sqliteai/sqlite-sync-wasm": "^3.49.2-sync-0.8.9", + "@sqliteai/sqlite-sync-wasm": "^3.49.2-sync-0.8.20", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", diff --git a/examples/sport-tracker-app/src/App.tsx b/examples/sport-tracker-app/src/App.tsx index 2cd6603..e315942 100644 --- a/examples/sport-tracker-app/src/App.tsx +++ b/examples/sport-tracker-app/src/App.tsx @@ -36,6 +36,7 @@ const AppContent: React.FC = () => { const [refreshTrigger, setRefreshTrigger] = useState(0); const [sqliteSyncVersion, setSqliteSyncVersion] = useState(""); const [sqliteVersion, setSqliteVersion] = useState(""); + const [loginError, setLoginError] = useState(""); // Coach mode - true when logged in user is named "coach" const isCoachMode = (session: UserSession | null): boolean => { @@ -392,6 +393,15 @@ const AppContent: React.FC = () => { ); } + if (loginError) { + return ( +
+

Login Error

+

{loginError}

+
+ ); + } + if (!isInitialized || loading) { return (
@@ -425,6 +435,7 @@ const AppContent: React.FC = () => { onLogout={handleLogout} onUsersLoad={loadUsers} onRefresh={handleRefreshData} + onError={setLoginError} /> Promise; onUsersLoad: () => void; onRefresh: () => void; + onError?: (error: string) => void; // Optional error handler } const UserLogin: React.FC = ({ @@ -24,6 +25,7 @@ const UserLogin: React.FC = ({ onLogout, onUsersLoad, onRefresh, + onError }) => { const { db } = useDatabase(); const [selectedUserId, setSelectedUserId] = useState(""); @@ -113,6 +115,7 @@ const UserLogin: React.FC = ({ error ); console.warn("SQLite Sync: Falling back to local refresh only"); + if(onError) onError("SQLite Sync - Failed to sync with SQLite Cloud: " + error); } } else { console.log( diff --git a/examples/sport-tracker-app/src/db/database.ts b/examples/sport-tracker-app/src/db/database.ts index ca71e17..39b06d0 100644 --- a/examples/sport-tracker-app/src/db/database.ts +++ b/examples/sport-tracker-app/src/db/database.ts @@ -97,7 +97,6 @@ export class Database { return this.sendMessage("sqliteSyncSetToken", token); } - async sqliteSyncNetworkSync(): Promise { return this.sendMessage("sqliteSyncNetworkSync"); } diff --git a/src/cloudsync.h b/src/cloudsync.h index 0b33ede..fe9b87f 100644 --- a/src/cloudsync.h +++ b/src/cloudsync.h @@ -16,7 +16,7 @@ #include "sqlite3.h" #endif -#define CLOUDSYNC_VERSION "0.8.12" +#define CLOUDSYNC_VERSION "0.8.20" int sqlite3_cloudsync_init (sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi); diff --git a/src/dbutils.c b/src/dbutils.c index dd7e13a..9edd10f 100644 --- a/src/dbutils.c +++ b/src/dbutils.c @@ -1,6 +1,6 @@ // // dbutils.c -// cloudsync_test +// cloudsync // // Created by Marco Bambini on 23/09/24. // diff --git a/src/network.m b/src/network.m index c9a9dc9..3bba42d 100644 --- a/src/network.m +++ b/src/network.m @@ -1,6 +1,6 @@ // // network.m -// cloudsync_network_test +// cloudsync // // Created by Marco Bambini on 23/05/25. // @@ -58,7 +58,7 @@ bool network_compute_endpoints (sqlite3_context *context, network_data *data, co } char *site_id = network_data_get_siteid(data); - char *port_or_default = (port) ? (char *)port.UTF8String : CLOUDSYNC_DEFAULT_ENDPOINT_PORT; + char *port_or_default = (port && strcmp(port.UTF8String, "8860") != 0) ? (char *)port.UTF8String : CLOUDSYNC_DEFAULT_ENDPOINT_PORT; NSString *check_endpoint = [NSString stringWithFormat:@"%s://%s:%s/%s%s/%s", scheme.UTF8String, host.UTF8String, port_or_default, CLOUDSYNC_ENDPOINT_PREFIX, database.UTF8String, site_id]; NSString *upload_endpoint = [NSString stringWithFormat: @"%s://%s:%s/%s%s/%s/%s", scheme.UTF8String, host.UTF8String, port_or_default, CLOUDSYNC_ENDPOINT_PREFIX, database.UTF8String, site_id, CLOUDSYNC_ENDPOINT_UPLOAD]; @@ -147,15 +147,19 @@ NETWORK_RESULT network_receive_buffer(network_data *data, const char *endpoint, } __block NSData *responseData = nil; - __block NSError *responseError = nil; + __block NSString *responseError = nil; __block NSInteger statusCode = 0; + __block NSInteger errorCode = 0; dispatch_semaphore_t sema = dispatch_semaphore_create(0); NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { responseData = data; - responseError = error; + if (error) { + responseError = [error localizedDescription]; + errorCode = [error code]; + } if ([response isKindOfClass:[NSHTTPURLResponse class]]) { statusCode = [(NSHTTPURLResponse *)response statusCode]; } @@ -166,9 +170,10 @@ NETWORK_RESULT network_receive_buffer(network_data *data, const char *endpoint, dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); if (!responseError && (statusCode >= 200 && statusCode < 300)) { - size_t len = [responseData length]; // check if OK should be returned - if (len == 0) return (NETWORK_RESULT){CLOUDSYNC_NETWORK_OK, NULL, 0, NULL, NULL}; + if (responseData == nil || [responseData length] == 0) { + return (NETWORK_RESULT){CLOUDSYNC_NETWORK_OK, NULL, 0, NULL, NULL}; + } // otherwise return a buffer NETWORK_RESULT result = {}; @@ -181,18 +186,31 @@ NETWORK_RESULT network_receive_buffer(network_data *data, const char *endpoint, result.buffer = (char *)responseData.bytes; result.xdata = (void *)CFBridgingRetain(responseData); } - result.blen = len; + result.blen = [responseData length]; result.xfree = network_buffer_cleanup; + return result; } // return error NETWORK_RESULT result = {}; - NSString *msg = (responseError) ? [responseError localizedDescription] : [NSString stringWithCString:"Unknown network URL" encoding:NSUTF8StringEncoding]; + NSString *msg = nil; + if (responseError) { + msg = responseError; + } else if (responseData && [responseData length] > 0) { + // Use the actual response body as the error message + msg = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding]; + if (!msg) { + msg = [NSString stringWithFormat:@"HTTP %ld error", (long)statusCode]; + } + } else { + msg = [NSString stringWithFormat:@"HTTP %ld error", (long)statusCode]; + } result.code = CLOUDSYNC_NETWORK_ERROR; result.buffer = (char *)msg.UTF8String; result.xdata = (void *)CFBridgingRetain(msg); result.xfree = network_buffer_cleanup; - result.blen = (responseError) ? (size_t)responseError.code : (size_t)statusCode; + result.blen = responseError ? (size_t)errorCode : (size_t)statusCode; + return result; } diff --git a/src/wasm.c b/src/wasm.c index 9c53835..2fc324f 100644 --- a/src/wasm.c +++ b/src/wasm.c @@ -1,3 +1,10 @@ +// +// wasm.c +// cloudsync +// +// Created by Gioele Cantoni on 25/06/25. +// + #ifdef SQLITE_WASM_EXTRA_INIT #define CLOUDSYNC_OMIT_CURL @@ -96,7 +103,6 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, } if (fetch->status >= 200 && fetch->status < 300) { - if (blen > 0 && buffer) { char *buf = (char*)malloc(blen + 1); if (buf) { @@ -112,9 +118,18 @@ NETWORK_RESULT network_receive_buffer (network_data *data, const char *endpoint, result.code = CLOUDSYNC_NETWORK_ERROR; if (fetch->statusText && fetch->statusText[0]) { result.buffer = strdup(fetch->statusText); + result.blen = sizeof(fetch->statusText); + result.xfree = free; + } else if (blen > 0 && buffer) { + char *buf = (char*)malloc(blen + 1); + if (buf) { + memcpy(buf, buffer, blen); + buf[blen] = 0; + result.buffer = buf; + result.blen = blen; + result.xfree = free; + } } - result.blen = sizeof(fetch->statusText); - result.xfree = free; } // cleanup diff --git a/test/main.c b/test/main.c index 2ed92a2..d8f9b1b 100644 --- a/test/main.c +++ b/test/main.c @@ -1,6 +1,6 @@ // // main.c -// sqlite-sync +// cloudsync // // Created by Gioele Cantoni on 05/06/25. // Set CONNECTION_STRING, APIKEY and WEBLITE environment variables before running this test. @@ -358,6 +358,7 @@ void* worker(void* arg) { int main (void) { int rc = SQLITE_OK; + remove(DB_PATH); // remove the database file if it exists cloudsync_memory_init(1);