From b6df02918afb1f1c4faa3fcd4236d55b9405429b Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Mon, 1 Dec 2025 15:31:50 -0700 Subject: [PATCH 01/17] build(deps): ditto-java (5.0.0-java-rc.2) --- java-server/build.gradle.kts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/java-server/build.gradle.kts b/java-server/build.gradle.kts index 4b81afcc5..d454f14fe 100644 --- a/java-server/build.gradle.kts +++ b/java-server/build.gradle.kts @@ -31,29 +31,29 @@ spotbugs { dependencies { // ditto-java artifact includes the Java API for Ditto - implementation("com.ditto:ditto-java:5.0.0-preview.3") + implementation("com.ditto:ditto-java:5.0.0-java-rc.2") // This will include binaries for all the supported platforms and architectures - implementation("com.ditto:ditto-binaries:5.0.0-preview.3") + implementation("com.ditto:ditto-binaries:5.0.0-java-rc.2") // To reduce your module artifact's size, consider including just the necessary platforms and architectures /* // macOS Apple Silicon - implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { + implementation("com.ditto:ditto-binaries:5.0.0-java-rc.2") { capabilities { requireCapability("com.ditto:ditto-binaries-macos-arm64") } } // Windows x86_64 - implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { + implementation("com.ditto:ditto-binaries:5.0.0-java-rc.2") { capabilities { requireCapability("com.ditto:ditto-binaries-windows-x64") } } // Linux x86_64 - implementation("com.ditto:ditto-binaries:5.0.0-preview.3") { + implementation("com.ditto:ditto-binaries:5.0.0-java-rc.2") { capabilities { requireCapability("com.ditto:ditto-binaries-linux-x64") } From 1b5f584cb1e8b90c24ab2a5cc13cee5ec4b12624 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Mon, 1 Dec 2025 15:48:33 -0700 Subject: [PATCH 02/17] refactor: use new DittoException instead of DittoError --- .../quickstart/service/DittoService.java | 21 +- .../quickstart/service/DittoTaskService.java | 215 ++++++++++++------ 2 files changed, 153 insertions(+), 83 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java index f5ca7d5c4..3ae8cd723 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java @@ -102,7 +102,7 @@ public void toggleSync() { try { boolean currentSyncState = mutableSyncStatePublisher.asFlux().blockFirst(); setSyncStateIntoDittoStore(!currentSyncState); - } catch (DittoError e) { + } catch (DittoException e) { throw new RuntimeException(e); } } @@ -146,15 +146,20 @@ private DittoStoreObserver setupAndObserveSyncState() { List items = result.getItems(); boolean newSyncState = false; if (!items.isEmpty()) { - newSyncState = items.get(0).getValue() - .get(DITTO_SYNC_STATE_ID) - .asBoolean(); + try { + newSyncState = items.get(0).getValue() + .get(DITTO_SYNC_STATE_ID) + .asBoolean(); + } catch (DittoException e) { + logger.error(e.getMessage()); + throw new RuntimeException(e); + } } if (newSyncState) { try { ditto.startSync(); - } catch (DittoError e) { + } catch (DittoException e) { throw new RuntimeException(e); } } else { @@ -163,12 +168,12 @@ private DittoStoreObserver setupAndObserveSyncState() { mutableSyncStatePublisher.tryEmitNext(newSyncState); }); - } catch (DittoError e) { + } catch (DittoException e) { throw new RuntimeException(e); } } - private void setSyncStateIntoDittoStore(boolean newState) throws DittoError { + private void setSyncStateIntoDittoStore(boolean newState) throws DittoException { CompletionStage future = ditto.getStore().execute( "UPDATE %s SET %s = :syncState".formatted(DITTO_SYNC_STATE_COLLECTION, DITTO_SYNC_STATE_ID), DittoCborSerializable.buildDictionary() @@ -178,7 +183,7 @@ private void setSyncStateIntoDittoStore(boolean newState) throws DittoError { try { future.toCompletableFuture().join().close(); - } catch (DittoError e) { + } catch (DittoException e) { throw new RuntimeException(e); } } diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 2ce001a85..4111de5a9 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -2,18 +2,20 @@ import com.ditto.java.*; import com.ditto.java.serialization.DittoCborSerializable; - import jakarta.annotation.Nonnull; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxSink; import java.io.IOException; import java.util.List; +import java.util.Objects; import java.util.UUID; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; + @Component public class DittoTaskService { + private static final String TASKS_COLLECTION_NAME = "tasks"; private final DittoService dittoService; @@ -24,20 +26,27 @@ public DittoTaskService(DittoService dittoService) { public void addTask(@Nonnull String title) { try { - dittoService.getDitto().getStore().execute( - "INSERT INTO %s DOCUMENTS (:newTask)".formatted(TASKS_COLLECTION_NAME), + dittoService + .getDitto() + .getStore() + .execute( + "INSERT INTO %s DOCUMENTS (:newTask)".formatted( + TASKS_COLLECTION_NAME + ), DittoCborSerializable.Dictionary.buildDictionary() - .put( - "newTask", - DittoCborSerializable.Dictionary.buildDictionary() - .put("_id", UUID.randomUUID().toString()) - .put("title", title) - .put("done", false) - .put("deleted", false) - .build() - ) - .build() - ).toCompletableFuture().join(); + .put( + "newTask", + DittoCborSerializable.Dictionary.buildDictionary() + .put("_id", UUID.randomUUID().toString()) + .put("title", title) + .put("done", false) + .put("deleted", false) + .build() + ) + .build() + ) + .toCompletableFuture() + .join(); } catch (Error e) { throw new RuntimeException(e); } @@ -45,36 +54,62 @@ public void addTask(@Nonnull String title) { public void toggleTaskDone(@Nonnull String taskId) { try { - DittoQueryResult tasks = dittoService.getDitto().getStore().execute( - "SELECT * FROM %s WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + DittoQueryResult tasks = dittoService + .getDitto() + .getStore() + .execute( + "SELECT * FROM %s WHERE _id = :taskId".formatted( + TASKS_COLLECTION_NAME + ), DittoCborSerializable.Dictionary.buildDictionary() - .put("taskId", taskId) - .build() - ).toCompletableFuture().join(); - - boolean isDone = tasks.getItems().get(0).getValue().get("done").asBoolean(); - - dittoService.getDitto().getStore().execute( - "UPDATE %s SET done = :done WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + .put("taskId", taskId) + .build() + ) + .toCompletableFuture() + .join(); + + boolean isDone = tasks + .getItems() + .get(0) + .getValue() + .get("done") + .asBoolean(); + + dittoService + .getDitto() + .getStore() + .execute( + "UPDATE %s SET done = :done WHERE _id = :taskId".formatted( + TASKS_COLLECTION_NAME + ), DittoCborSerializable.Dictionary.buildDictionary() - .put("done", !isDone) - .put("taskId", taskId) - .build() - ).toCompletableFuture().join(); - } catch (Error e) { + .put("done", !isDone) + .put("taskId", taskId) + .build() + ) + .toCompletableFuture() + .join(); + } catch (Error | DittoException e) { throw new RuntimeException(e); } } public void deleteTask(@Nonnull String taskId) { try { - dittoService.getDitto().getStore().execute( - "UPDATE %s SET deleted = :deleted WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + dittoService + .getDitto() + .getStore() + .execute( + "UPDATE %s SET deleted = :deleted WHERE _id = :taskId".formatted( + TASKS_COLLECTION_NAME + ), DittoCborSerializable.Dictionary.buildDictionary() - .put("deleted", true) - .put("taskId", taskId) - .build() - ).toCompletableFuture().join(); + .put("deleted", true) + .put("taskId", taskId) + .build() + ) + .toCompletableFuture() + .join(); } catch (Error e) { throw new RuntimeException(e); } @@ -82,56 +117,86 @@ public void deleteTask(@Nonnull String taskId) { public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { try { - dittoService.getDitto().getStore().execute( - "UPDATE %s SET title = :title WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + dittoService + .getDitto() + .getStore() + .execute( + "UPDATE %s SET title = :title WHERE _id = :taskId".formatted( + TASKS_COLLECTION_NAME + ), DittoCborSerializable.Dictionary.buildDictionary() - .put("title", newTitle) - .put("taskId", taskId) - .build() - ).toCompletableFuture().join(); - } catch (Error e) { + .put("title", newTitle) + .put("taskId", taskId) + .build() + ) + .toCompletableFuture() + .join(); + } catch (Error e) { throw new RuntimeException(e); } } @Nonnull public Flux> observeAll() { - final String subscriptionQuery = "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); + final String subscriptionQuery = + "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); final String displayQuery = subscriptionQuery + " ORDER BY title ASC"; - return Flux.create(emitter -> { - Ditto ditto = dittoService.getDitto(); - try { - DittoSyncSubscription subscription = ditto.getSync().registerSubscription(subscriptionQuery); - DittoStoreObserver observer = ditto.getStore().registerObserver(displayQuery, results -> - emitter.next(results.getItems().stream().map(this::itemToTask).toList())); - - emitter.onDispose(() -> { - // TODO: Can't just catch, this potentially leaks the `observer` resource. - try { - subscription.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - try { - observer.close(); - } catch (DittoError e) { - throw new RuntimeException(e); - } - }); - } catch (DittoError e) { - emitter.error(e); - } - }, FluxSink.OverflowStrategy.LATEST); + return Flux.create( + emitter -> { + Ditto ditto = dittoService.getDitto(); + try { + DittoSyncSubscription subscription = ditto + .getSync() + .registerSubscription(subscriptionQuery); + DittoStoreObserver observer = ditto + .getStore() + .registerObserver(displayQuery, results -> + emitter.next( + results + .getItems() + .stream() + .map(item -> { + try { + return this.itemToTask(item); + } catch (Exception e) { + emitter.error(e); + return null; + } + }) + .filter(Objects::nonNull) + .toList() + ) + ); + + emitter.onDispose(() -> { + // TODO: Can't just catch, this potentially leaks the `observer` resource. + try { + subscription.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + try { + observer.close(); + } catch (DittoException e) { + throw new RuntimeException(e); + } + }); + } catch (DittoException e) { + emitter.error(e); + } + }, + FluxSink.OverflowStrategy.LATEST + ); } - private Task itemToTask(@Nonnull DittoQueryResultItem item) { + private Task itemToTask(@Nonnull DittoQueryResultItem item) throws DittoException { DittoCborSerializable.Dictionary value = item.getValue(); return new Task( - value.get("_id").asString(), - value.get("title").asString(), - value.get("done").asBoolean(), - value.get("deleted").asBoolean() + value.get("_id").asString(), + value.get("title").asString(), + value.get("done").asBoolean(), + value.get("deleted").asBoolean() ); } } From 845d8f35d57d903c86a6778bc235aa7461003a53 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 5 Dec 2025 08:32:16 -0700 Subject: [PATCH 03/17] Update java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../example/spring/quickstart/service/DittoTaskService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 4111de5a9..e2fa6aa53 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -131,7 +131,7 @@ public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { ) .toCompletableFuture() .join(); - } catch (Error e) { + } catch (DittoException | Error e) { throw new RuntimeException(e); } } From b90112b15dbe1d2f772015d92b88fe710eb2ab97 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 5 Dec 2025 08:32:41 -0700 Subject: [PATCH 04/17] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../example/spring/quickstart/service/DittoTaskService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index e2fa6aa53..94fc9b124 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -110,7 +110,7 @@ public void deleteTask(@Nonnull String taskId) { ) .toCompletableFuture() .join(); - } catch (Error e) { + } catch (Error | DittoException e) { throw new RuntimeException(e); } } From 5a50e686a52da866d71189a2db10d4482b13e60a Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 5 Dec 2025 08:33:37 -0700 Subject: [PATCH 05/17] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../example/spring/quickstart/service/DittoTaskService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 94fc9b124..bafd21759 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -161,10 +161,9 @@ public Flux> observeAll() { return this.itemToTask(item); } catch (Exception e) { emitter.error(e); - return null; + throw new RuntimeException(e); } }) - .filter(Objects::nonNull) .toList() ) ); From be4540b47a42114b2a0d9f4fe07c0fd271b824ca Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 5 Dec 2025 15:28:01 -0700 Subject: [PATCH 06/17] fix(java-server): remove bad copilot suggestions --- .../example/spring/quickstart/service/DittoTaskService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index bafd21759..a65cc3e1e 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -110,7 +110,7 @@ public void deleteTask(@Nonnull String taskId) { ) .toCompletableFuture() .join(); - } catch (Error | DittoException e) { + } catch (Error e) { throw new RuntimeException(e); } } @@ -131,7 +131,7 @@ public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { ) .toCompletableFuture() .join(); - } catch (DittoException | Error e) { + } catch (Error e) { throw new RuntimeException(e); } } From 0b156773361c85d8b7d79f1cfb3144904a1e3adb Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Sun, 7 Dec 2025 08:07:19 -0700 Subject: [PATCH 07/17] feat(java-server): explicitly close all results & observers --- .../quickstart/service/DittoTaskService.java | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index a65cc3e1e..5cc30cf5e 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -3,16 +3,14 @@ import com.ditto.java.*; import com.ditto.java.serialization.DittoCborSerializable; import jakarta.annotation.Nonnull; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; import java.io.IOException; import java.util.List; -import java.util.Objects; import java.util.UUID; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxSink; - @Component public class DittoTaskService { @@ -26,7 +24,7 @@ public DittoTaskService(DittoService dittoService) { public void addTask(@Nonnull String title) { try { - dittoService + DittoQueryResult result = dittoService .getDitto() .getStore() .execute( @@ -47,14 +45,16 @@ public void addTask(@Nonnull String title) { ) .toCompletableFuture() .join(); - } catch (Error e) { + result.close(); + } catch (Error | DittoException e) { throw new RuntimeException(e); } } public void toggleTaskDone(@Nonnull String taskId) { try { - DittoQueryResult tasks = dittoService + boolean isDone; + try (DittoQueryResult tasks = dittoService .getDitto() .getStore() .execute( @@ -66,16 +66,17 @@ public void toggleTaskDone(@Nonnull String taskId) { .build() ) .toCompletableFuture() - .join(); - - boolean isDone = tasks - .getItems() - .get(0) - .getValue() - .get("done") - .asBoolean(); - - dittoService + .join() + ) { + isDone = tasks + .getItems() + .get(0) + .getValue() + .get("done") + .asBoolean(); + } + + DittoQueryResult result = dittoService .getDitto() .getStore() .execute( @@ -89,6 +90,7 @@ public void toggleTaskDone(@Nonnull String taskId) { ) .toCompletableFuture() .join(); + result.close(); } catch (Error | DittoException e) { throw new RuntimeException(e); } @@ -96,7 +98,7 @@ public void toggleTaskDone(@Nonnull String taskId) { public void deleteTask(@Nonnull String taskId) { try { - dittoService + DittoQueryResult result = dittoService .getDitto() .getStore() .execute( @@ -110,14 +112,15 @@ public void deleteTask(@Nonnull String taskId) { ) .toCompletableFuture() .join(); - } catch (Error e) { + result.close(); + } catch (Error | DittoException e) { throw new RuntimeException(e); } } public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { try { - dittoService + DittoQueryResult result = dittoService .getDitto() .getStore() .execute( @@ -131,7 +134,8 @@ public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { ) .toCompletableFuture() .join(); - } catch (Error e) { + result.close(); + } catch (Error| DittoException e) { throw new RuntimeException(e); } } @@ -146,9 +150,12 @@ public Flux> observeAll() { emitter -> { Ditto ditto = dittoService.getDitto(); try { + @SuppressWarnings("resource") DittoSyncSubscription subscription = ditto .getSync() .registerSubscription(subscriptionQuery); + + @SuppressWarnings("resource") DittoStoreObserver observer = ditto .getStore() .registerObserver(displayQuery, results -> From f2fd7de37c717b116347d0746215f030b54c75ae Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 11 Dec 2025 09:17:54 -0700 Subject: [PATCH 08/17] refactor(java-server): handle new DittoException type # Conflicts: # java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java # java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java --- .../quickstart/service/DittoService.java | 14 +- .../quickstart/service/DittoTaskService.java | 223 ++++++------------ 2 files changed, 85 insertions(+), 152 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java index 3ae8cd723..d4a5ebf36 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java @@ -47,6 +47,7 @@ public class DittoService implements DisposableBean { */ DittoConfig dittoConfig = new DittoConfig.Builder(DittoSecretsConfiguration.DITTO_APP_ID) + .persistenceDirectory("C:\\ditto-quickstart") .serverConnect(DittoSecretsConfiguration.DITTO_AUTH_URL) .build(); @@ -60,7 +61,7 @@ public class DittoService implements DisposableBean { ).thenRun(() -> { }) ); - this.ditto.setDeviceName("Spring Java"); + this.ditto.setDeviceName("Java"); this.ditto.updateTransportConfig(config -> { config.connect(connect -> { @@ -99,8 +100,8 @@ public Flux getSyncState() { } public void toggleSync() { + boolean currentSyncState = mutableSyncStatePublisher.asFlux().blockFirst(); try { - boolean currentSyncState = mutableSyncStatePublisher.asFlux().blockFirst(); setSyncStateIntoDittoStore(!currentSyncState); } catch (DittoException e) { throw new RuntimeException(e); @@ -145,15 +146,14 @@ private DittoStoreObserver setupAndObserveSyncState() { (result) -> { List items = result.getItems(); boolean newSyncState = false; - if (!items.isEmpty()) { - try { + try { + if (!items.isEmpty()) { newSyncState = items.get(0).getValue() .get(DITTO_SYNC_STATE_ID) .asBoolean(); - } catch (DittoException e) { - logger.error(e.getMessage()); - throw new RuntimeException(e); } + } catch (DittoException e) { + System.err.println("Error: " + e); } if (newSyncState) { diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 5cc30cf5e..73554a383 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -2,6 +2,7 @@ import com.ditto.java.*; import com.ditto.java.serialization.DittoCborSerializable; + import jakarta.annotation.Nonnull; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; @@ -13,7 +14,6 @@ @Component public class DittoTaskService { - private static final String TASKS_COLLECTION_NAME = "tasks"; private final DittoService dittoService; @@ -24,73 +24,43 @@ public DittoTaskService(DittoService dittoService) { public void addTask(@Nonnull String title) { try { - DittoQueryResult result = dittoService - .getDitto() - .getStore() - .execute( - "INSERT INTO %s DOCUMENTS (:newTask)".formatted( - TASKS_COLLECTION_NAME - ), + dittoService.getDitto().getStore().execute( + "INSERT INTO %s DOCUMENTS (:newTask)".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put( - "newTask", - DittoCborSerializable.Dictionary.buildDictionary() - .put("_id", UUID.randomUUID().toString()) - .put("title", title) - .put("done", false) - .put("deleted", false) - .build() - ) - .build() - ) - .toCompletableFuture() - .join(); - result.close(); - } catch (Error | DittoException e) { + .put( + "newTask", + DittoCborSerializable.Dictionary.buildDictionary() + .put("_id", UUID.randomUUID().toString()) + .put("title", title) + .put("done", false) + .put("deleted", false) + .build() + ) + .build() + ).toCompletableFuture().join(); + } catch (Error e) { throw new RuntimeException(e); } } public void toggleTaskDone(@Nonnull String taskId) { try { - boolean isDone; - try (DittoQueryResult tasks = dittoService - .getDitto() - .getStore() - .execute( - "SELECT * FROM %s WHERE _id = :taskId".formatted( - TASKS_COLLECTION_NAME - ), + DittoQueryResult tasks = dittoService.getDitto().getStore().execute( + "SELECT * FROM %s WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("taskId", taskId) - .build() - ) - .toCompletableFuture() - .join() - ) { - isDone = tasks - .getItems() - .get(0) - .getValue() - .get("done") - .asBoolean(); - } + .put("taskId", taskId) + .build() + ).toCompletableFuture().join(); + + boolean isDone = tasks.getItems().get(0).getValue().get("done").asBoolean(); - DittoQueryResult result = dittoService - .getDitto() - .getStore() - .execute( - "UPDATE %s SET done = :done WHERE _id = :taskId".formatted( - TASKS_COLLECTION_NAME - ), + dittoService.getDitto().getStore().execute( + "UPDATE %s SET done = :done WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("done", !isDone) - .put("taskId", taskId) - .build() - ) - .toCompletableFuture() - .join(); - result.close(); + .put("done", !isDone) + .put("taskId", taskId) + .build() + ).toCompletableFuture().join(); } catch (Error | DittoException e) { throw new RuntimeException(e); } @@ -98,111 +68,74 @@ public void toggleTaskDone(@Nonnull String taskId) { public void deleteTask(@Nonnull String taskId) { try { - DittoQueryResult result = dittoService - .getDitto() - .getStore() - .execute( - "UPDATE %s SET deleted = :deleted WHERE _id = :taskId".formatted( - TASKS_COLLECTION_NAME - ), + dittoService.getDitto().getStore().execute( + "UPDATE %s SET deleted = :deleted WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("deleted", true) - .put("taskId", taskId) - .build() - ) - .toCompletableFuture() - .join(); - result.close(); - } catch (Error | DittoException e) { + .put("deleted", true) + .put("taskId", taskId) + .build() + ).toCompletableFuture().join(); + } catch (Error e) { throw new RuntimeException(e); } } public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { try { - DittoQueryResult result = dittoService - .getDitto() - .getStore() - .execute( - "UPDATE %s SET title = :title WHERE _id = :taskId".formatted( - TASKS_COLLECTION_NAME - ), + dittoService.getDitto().getStore().execute( + "UPDATE %s SET title = :title WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("title", newTitle) - .put("taskId", taskId) - .build() - ) - .toCompletableFuture() - .join(); - result.close(); - } catch (Error| DittoException e) { + .put("title", newTitle) + .put("taskId", taskId) + .build() + ).toCompletableFuture().join(); + } catch (Error e) { throw new RuntimeException(e); } } @Nonnull public Flux> observeAll() { - final String subscriptionQuery = - "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); + final String subscriptionQuery = "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); final String displayQuery = subscriptionQuery + " ORDER BY title ASC"; - return Flux.create( - emitter -> { - Ditto ditto = dittoService.getDitto(); - try { - @SuppressWarnings("resource") - DittoSyncSubscription subscription = ditto - .getSync() - .registerSubscription(subscriptionQuery); - - @SuppressWarnings("resource") - DittoStoreObserver observer = ditto - .getStore() - .registerObserver(displayQuery, results -> - emitter.next( - results - .getItems() - .stream() - .map(item -> { - try { - return this.itemToTask(item); - } catch (Exception e) { - emitter.error(e); - throw new RuntimeException(e); - } - }) - .toList() - ) - ); - - emitter.onDispose(() -> { - // TODO: Can't just catch, this potentially leaks the `observer` resource. - try { - subscription.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - try { - observer.close(); - } catch (DittoException e) { - throw new RuntimeException(e); - } - }); - } catch (DittoException e) { - emitter.error(e); - } - }, - FluxSink.OverflowStrategy.LATEST - ); + return Flux.create(emitter -> { + Ditto ditto = dittoService.getDitto(); + try { + DittoSyncSubscription subscription = ditto.getSync().registerSubscription(subscriptionQuery); + DittoStoreObserver observer = ditto.getStore().registerObserver(displayQuery, results -> + emitter.next(results.getItems().stream().map(this::itemToTask).toList())); + + emitter.onDispose(() -> { + // TODO: Can't just catch, this potentially leaks the `observer` resource. + try { + subscription.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + try { + observer.close(); + } catch (DittoException e) { + throw new RuntimeException(e); + } + }); + } catch (DittoException e) { + emitter.error(e); + } + }, FluxSink.OverflowStrategy.LATEST); } - private Task itemToTask(@Nonnull DittoQueryResultItem item) throws DittoException { + private Task itemToTask(@Nonnull DittoQueryResultItem item) { DittoCborSerializable.Dictionary value = item.getValue(); - return new Task( - value.get("_id").asString(), - value.get("title").asString(), - value.get("done").asBoolean(), - value.get("deleted").asBoolean() - ); + try { + return new Task( + value.get("_id").asString(), + value.get("title").asString(), + value.get("done").asBoolean(), + value.get("deleted").asBoolean() + ); + } catch (DittoException e) { + throw new RuntimeException(e); + } } } From dc053242e9ef882d2223ee594bb8dc75f952729a Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Tue, 16 Dec 2025 10:17:44 -0700 Subject: [PATCH 09/17] feat(java-server): add winsw service config --- java-server/.gitignore | 4 ++++ java-server/ditto-service.xml | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 java-server/ditto-service.xml diff --git a/java-server/.gitignore b/java-server/.gitignore index 77b721e1d..212311511 100644 --- a/java-server/.gitignore +++ b/java-server/.gitignore @@ -1,3 +1,7 @@ .kotlin/ bin/ build/ +ditto-service.err.log +ditto-service.exe +ditto-service.out.log +ditto-service.wrapper.log diff --git a/java-server/ditto-service.xml b/java-server/ditto-service.xml new file mode 100644 index 000000000..4a21030fe --- /dev/null +++ b/java-server/ditto-service.xml @@ -0,0 +1,24 @@ + + ditto-service + Ditto Java Quickstart App + Description of your application + + java + -jar "%BASE%\build\libs\quickstart-java-0.0.1-SNAPSHOT.jar" + + + %BASE% + + + + + + + 10240 + 8 + + + + + + From 9b6a569a98c4d323b1f4cbe1b165fca4d8c070f8 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Mon, 1 Dec 2025 15:48:33 -0700 Subject: [PATCH 10/17] refactor: use new DittoException instead of DittoError --- .../quickstart/service/DittoService.java | 7 +- .../quickstart/service/DittoTaskService.java | 205 ++++++++++++------ 2 files changed, 140 insertions(+), 72 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java index d4a5ebf36..8aa3506b5 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java @@ -146,11 +146,14 @@ private DittoStoreObserver setupAndObserveSyncState() { (result) -> { List items = result.getItems(); boolean newSyncState = false; - try { - if (!items.isEmpty()) { + if (!items.isEmpty()) { + try { newSyncState = items.get(0).getValue() .get(DITTO_SYNC_STATE_ID) .asBoolean(); + } catch (DittoException e) { + logger.error(e.getMessage()); + throw new RuntimeException(e); } } catch (DittoException e) { System.err.println("Error: " + e); diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 73554a383..4aac7a682 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -2,18 +2,20 @@ import com.ditto.java.*; import com.ditto.java.serialization.DittoCborSerializable; - import jakarta.annotation.Nonnull; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxSink; import java.io.IOException; import java.util.List; +import java.util.Objects; import java.util.UUID; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; + @Component public class DittoTaskService { + private static final String TASKS_COLLECTION_NAME = "tasks"; private final DittoService dittoService; @@ -24,20 +26,27 @@ public DittoTaskService(DittoService dittoService) { public void addTask(@Nonnull String title) { try { - dittoService.getDitto().getStore().execute( - "INSERT INTO %s DOCUMENTS (:newTask)".formatted(TASKS_COLLECTION_NAME), + dittoService + .getDitto() + .getStore() + .execute( + "INSERT INTO %s DOCUMENTS (:newTask)".formatted( + TASKS_COLLECTION_NAME + ), DittoCborSerializable.Dictionary.buildDictionary() - .put( - "newTask", - DittoCborSerializable.Dictionary.buildDictionary() - .put("_id", UUID.randomUUID().toString()) - .put("title", title) - .put("done", false) - .put("deleted", false) - .build() - ) - .build() - ).toCompletableFuture().join(); + .put( + "newTask", + DittoCborSerializable.Dictionary.buildDictionary() + .put("_id", UUID.randomUUID().toString()) + .put("title", title) + .put("done", false) + .put("deleted", false) + .build() + ) + .build() + ) + .toCompletableFuture() + .join(); } catch (Error e) { throw new RuntimeException(e); } @@ -45,22 +54,41 @@ public void addTask(@Nonnull String title) { public void toggleTaskDone(@Nonnull String taskId) { try { - DittoQueryResult tasks = dittoService.getDitto().getStore().execute( - "SELECT * FROM %s WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + DittoQueryResult tasks = dittoService + .getDitto() + .getStore() + .execute( + "SELECT * FROM %s WHERE _id = :taskId".formatted( + TASKS_COLLECTION_NAME + ), DittoCborSerializable.Dictionary.buildDictionary() - .put("taskId", taskId) - .build() - ).toCompletableFuture().join(); - - boolean isDone = tasks.getItems().get(0).getValue().get("done").asBoolean(); - - dittoService.getDitto().getStore().execute( - "UPDATE %s SET done = :done WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + .put("taskId", taskId) + .build() + ) + .toCompletableFuture() + .join(); + + boolean isDone = tasks + .getItems() + .get(0) + .getValue() + .get("done") + .asBoolean(); + + dittoService + .getDitto() + .getStore() + .execute( + "UPDATE %s SET done = :done WHERE _id = :taskId".formatted( + TASKS_COLLECTION_NAME + ), DittoCborSerializable.Dictionary.buildDictionary() - .put("done", !isDone) - .put("taskId", taskId) - .build() - ).toCompletableFuture().join(); + .put("done", !isDone) + .put("taskId", taskId) + .build() + ) + .toCompletableFuture() + .join(); } catch (Error | DittoException e) { throw new RuntimeException(e); } @@ -68,13 +96,20 @@ public void toggleTaskDone(@Nonnull String taskId) { public void deleteTask(@Nonnull String taskId) { try { - dittoService.getDitto().getStore().execute( - "UPDATE %s SET deleted = :deleted WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + dittoService + .getDitto() + .getStore() + .execute( + "UPDATE %s SET deleted = :deleted WHERE _id = :taskId".formatted( + TASKS_COLLECTION_NAME + ), DittoCborSerializable.Dictionary.buildDictionary() - .put("deleted", true) - .put("taskId", taskId) - .build() - ).toCompletableFuture().join(); + .put("deleted", true) + .put("taskId", taskId) + .build() + ) + .toCompletableFuture() + .join(); } catch (Error e) { throw new RuntimeException(e); } @@ -82,50 +117,80 @@ public void deleteTask(@Nonnull String taskId) { public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { try { - dittoService.getDitto().getStore().execute( - "UPDATE %s SET title = :title WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), + dittoService + .getDitto() + .getStore() + .execute( + "UPDATE %s SET title = :title WHERE _id = :taskId".formatted( + TASKS_COLLECTION_NAME + ), DittoCborSerializable.Dictionary.buildDictionary() - .put("title", newTitle) - .put("taskId", taskId) - .build() - ).toCompletableFuture().join(); - } catch (Error e) { + .put("title", newTitle) + .put("taskId", taskId) + .build() + ) + .toCompletableFuture() + .join(); + } catch (Error e) { throw new RuntimeException(e); } } @Nonnull public Flux> observeAll() { - final String subscriptionQuery = "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); + final String subscriptionQuery = + "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); final String displayQuery = subscriptionQuery + " ORDER BY title ASC"; - return Flux.create(emitter -> { - Ditto ditto = dittoService.getDitto(); - try { - DittoSyncSubscription subscription = ditto.getSync().registerSubscription(subscriptionQuery); - DittoStoreObserver observer = ditto.getStore().registerObserver(displayQuery, results -> - emitter.next(results.getItems().stream().map(this::itemToTask).toList())); - - emitter.onDispose(() -> { - // TODO: Can't just catch, this potentially leaks the `observer` resource. - try { - subscription.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - try { - observer.close(); - } catch (DittoException e) { - throw new RuntimeException(e); - } - }); - } catch (DittoException e) { - emitter.error(e); - } - }, FluxSink.OverflowStrategy.LATEST); + return Flux.create( + emitter -> { + Ditto ditto = dittoService.getDitto(); + try { + DittoSyncSubscription subscription = ditto + .getSync() + .registerSubscription(subscriptionQuery); + DittoStoreObserver observer = ditto + .getStore() + .registerObserver(displayQuery, results -> + emitter.next( + results + .getItems() + .stream() + .map(item -> { + try { + return this.itemToTask(item); + } catch (Exception e) { + emitter.error(e); + return null; + } + }) + .filter(Objects::nonNull) + .toList() + ) + ); + + emitter.onDispose(() -> { + // TODO: Can't just catch, this potentially leaks the `observer` resource. + try { + subscription.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + try { + observer.close(); + } catch (DittoException e) { + throw new RuntimeException(e); + } + }); + } catch (DittoException e) { + emitter.error(e); + } + }, + FluxSink.OverflowStrategy.LATEST + ); } - private Task itemToTask(@Nonnull DittoQueryResultItem item) { + private Task itemToTask(@Nonnull DittoQueryResultItem item) throws DittoException { DittoCborSerializable.Dictionary value = item.getValue(); try { return new Task( From 27f574ad22d6d96eacf23f6198d2fd7443bff423 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 5 Dec 2025 08:32:16 -0700 Subject: [PATCH 11/17] Update java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../example/spring/quickstart/service/DittoTaskService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 4aac7a682..9134ea7ae 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -131,7 +131,7 @@ public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { ) .toCompletableFuture() .join(); - } catch (Error e) { + } catch (DittoException | Error e) { throw new RuntimeException(e); } } From a59ae5a2f25e7baf9fee21ec1b1e401c783f889e Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 5 Dec 2025 08:32:41 -0700 Subject: [PATCH 12/17] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../example/spring/quickstart/service/DittoTaskService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 9134ea7ae..acdc91711 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -110,7 +110,7 @@ public void deleteTask(@Nonnull String taskId) { ) .toCompletableFuture() .join(); - } catch (Error e) { + } catch (Error | DittoException e) { throw new RuntimeException(e); } } From 7fd6102dcddd98cdbc5b71ac3681162fbf992598 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 5 Dec 2025 08:33:37 -0700 Subject: [PATCH 13/17] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../example/spring/quickstart/service/DittoTaskService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index acdc91711..1bf65aaab 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -161,10 +161,9 @@ public Flux> observeAll() { return this.itemToTask(item); } catch (Exception e) { emitter.error(e); - return null; + throw new RuntimeException(e); } }) - .filter(Objects::nonNull) .toList() ) ); From b03b52afdbb8948dc04759314a135bb9fdc1b5c1 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Fri, 5 Dec 2025 15:28:01 -0700 Subject: [PATCH 14/17] fix(java-server): remove bad copilot suggestions --- .../example/spring/quickstart/service/DittoTaskService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 1bf65aaab..5c9a1578d 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -110,7 +110,7 @@ public void deleteTask(@Nonnull String taskId) { ) .toCompletableFuture() .join(); - } catch (Error | DittoException e) { + } catch (Error e) { throw new RuntimeException(e); } } @@ -131,7 +131,7 @@ public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { ) .toCompletableFuture() .join(); - } catch (DittoException | Error e) { + } catch (Error e) { throw new RuntimeException(e); } } From 99e5f1f4624d1ed99989169bf5c4a5a10b4a7e65 Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Sun, 7 Dec 2025 08:07:19 -0700 Subject: [PATCH 15/17] feat(java-server): explicitly close all results & observers --- .../quickstart/service/DittoTaskService.java | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 5c9a1578d..20691a4ce 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -3,16 +3,14 @@ import com.ditto.java.*; import com.ditto.java.serialization.DittoCborSerializable; import jakarta.annotation.Nonnull; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; import java.io.IOException; import java.util.List; -import java.util.Objects; import java.util.UUID; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.FluxSink; - @Component public class DittoTaskService { @@ -26,7 +24,7 @@ public DittoTaskService(DittoService dittoService) { public void addTask(@Nonnull String title) { try { - dittoService + DittoQueryResult result = dittoService .getDitto() .getStore() .execute( @@ -47,14 +45,16 @@ public void addTask(@Nonnull String title) { ) .toCompletableFuture() .join(); - } catch (Error e) { + result.close(); + } catch (Error | DittoException e) { throw new RuntimeException(e); } } public void toggleTaskDone(@Nonnull String taskId) { try { - DittoQueryResult tasks = dittoService + boolean isDone; + try (DittoQueryResult tasks = dittoService .getDitto() .getStore() .execute( @@ -66,16 +66,17 @@ public void toggleTaskDone(@Nonnull String taskId) { .build() ) .toCompletableFuture() - .join(); - - boolean isDone = tasks - .getItems() - .get(0) - .getValue() - .get("done") - .asBoolean(); - - dittoService + .join() + ) { + isDone = tasks + .getItems() + .get(0) + .getValue() + .get("done") + .asBoolean(); + } + + DittoQueryResult result = dittoService .getDitto() .getStore() .execute( @@ -89,6 +90,7 @@ public void toggleTaskDone(@Nonnull String taskId) { ) .toCompletableFuture() .join(); + result.close(); } catch (Error | DittoException e) { throw new RuntimeException(e); } @@ -96,7 +98,7 @@ public void toggleTaskDone(@Nonnull String taskId) { public void deleteTask(@Nonnull String taskId) { try { - dittoService + DittoQueryResult result = dittoService .getDitto() .getStore() .execute( @@ -110,14 +112,15 @@ public void deleteTask(@Nonnull String taskId) { ) .toCompletableFuture() .join(); - } catch (Error e) { + result.close(); + } catch (Error | DittoException e) { throw new RuntimeException(e); } } public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { try { - dittoService + DittoQueryResult result = dittoService .getDitto() .getStore() .execute( @@ -131,7 +134,8 @@ public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { ) .toCompletableFuture() .join(); - } catch (Error e) { + result.close(); + } catch (Error| DittoException e) { throw new RuntimeException(e); } } @@ -146,9 +150,12 @@ public Flux> observeAll() { emitter -> { Ditto ditto = dittoService.getDitto(); try { + @SuppressWarnings("resource") DittoSyncSubscription subscription = ditto .getSync() .registerSubscription(subscriptionQuery); + + @SuppressWarnings("resource") DittoStoreObserver observer = ditto .getStore() .registerObserver(displayQuery, results -> From a766759d2081c45473d8850c31727e890330c02c Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 11 Dec 2025 09:17:54 -0700 Subject: [PATCH 16/17] refactor(java-server): handle new DittoException type # Conflicts: # java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java # java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java --- .../quickstart/service/DittoService.java | 7 +- .../quickstart/service/DittoTaskService.java | 207 ++++++------------ 2 files changed, 70 insertions(+), 144 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java index 8aa3506b5..d4a5ebf36 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java @@ -146,14 +146,11 @@ private DittoStoreObserver setupAndObserveSyncState() { (result) -> { List items = result.getItems(); boolean newSyncState = false; - if (!items.isEmpty()) { - try { + try { + if (!items.isEmpty()) { newSyncState = items.get(0).getValue() .get(DITTO_SYNC_STATE_ID) .asBoolean(); - } catch (DittoException e) { - logger.error(e.getMessage()); - throw new RuntimeException(e); } } catch (DittoException e) { System.err.println("Error: " + e); diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java index 20691a4ce..73554a383 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoTaskService.java @@ -2,6 +2,7 @@ import com.ditto.java.*; import com.ditto.java.serialization.DittoCborSerializable; + import jakarta.annotation.Nonnull; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; @@ -13,7 +14,6 @@ @Component public class DittoTaskService { - private static final String TASKS_COLLECTION_NAME = "tasks"; private final DittoService dittoService; @@ -24,73 +24,43 @@ public DittoTaskService(DittoService dittoService) { public void addTask(@Nonnull String title) { try { - DittoQueryResult result = dittoService - .getDitto() - .getStore() - .execute( - "INSERT INTO %s DOCUMENTS (:newTask)".formatted( - TASKS_COLLECTION_NAME - ), + dittoService.getDitto().getStore().execute( + "INSERT INTO %s DOCUMENTS (:newTask)".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put( - "newTask", - DittoCborSerializable.Dictionary.buildDictionary() - .put("_id", UUID.randomUUID().toString()) - .put("title", title) - .put("done", false) - .put("deleted", false) - .build() - ) - .build() - ) - .toCompletableFuture() - .join(); - result.close(); - } catch (Error | DittoException e) { + .put( + "newTask", + DittoCborSerializable.Dictionary.buildDictionary() + .put("_id", UUID.randomUUID().toString()) + .put("title", title) + .put("done", false) + .put("deleted", false) + .build() + ) + .build() + ).toCompletableFuture().join(); + } catch (Error e) { throw new RuntimeException(e); } } public void toggleTaskDone(@Nonnull String taskId) { try { - boolean isDone; - try (DittoQueryResult tasks = dittoService - .getDitto() - .getStore() - .execute( - "SELECT * FROM %s WHERE _id = :taskId".formatted( - TASKS_COLLECTION_NAME - ), + DittoQueryResult tasks = dittoService.getDitto().getStore().execute( + "SELECT * FROM %s WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("taskId", taskId) - .build() - ) - .toCompletableFuture() - .join() - ) { - isDone = tasks - .getItems() - .get(0) - .getValue() - .get("done") - .asBoolean(); - } + .put("taskId", taskId) + .build() + ).toCompletableFuture().join(); - DittoQueryResult result = dittoService - .getDitto() - .getStore() - .execute( - "UPDATE %s SET done = :done WHERE _id = :taskId".formatted( - TASKS_COLLECTION_NAME - ), + boolean isDone = tasks.getItems().get(0).getValue().get("done").asBoolean(); + + dittoService.getDitto().getStore().execute( + "UPDATE %s SET done = :done WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("done", !isDone) - .put("taskId", taskId) - .build() - ) - .toCompletableFuture() - .join(); - result.close(); + .put("done", !isDone) + .put("taskId", taskId) + .build() + ).toCompletableFuture().join(); } catch (Error | DittoException e) { throw new RuntimeException(e); } @@ -98,105 +68,64 @@ public void toggleTaskDone(@Nonnull String taskId) { public void deleteTask(@Nonnull String taskId) { try { - DittoQueryResult result = dittoService - .getDitto() - .getStore() - .execute( - "UPDATE %s SET deleted = :deleted WHERE _id = :taskId".formatted( - TASKS_COLLECTION_NAME - ), + dittoService.getDitto().getStore().execute( + "UPDATE %s SET deleted = :deleted WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("deleted", true) - .put("taskId", taskId) - .build() - ) - .toCompletableFuture() - .join(); - result.close(); - } catch (Error | DittoException e) { + .put("deleted", true) + .put("taskId", taskId) + .build() + ).toCompletableFuture().join(); + } catch (Error e) { throw new RuntimeException(e); } } public void updateTask(@Nonnull String taskId, @Nonnull String newTitle) { try { - DittoQueryResult result = dittoService - .getDitto() - .getStore() - .execute( - "UPDATE %s SET title = :title WHERE _id = :taskId".formatted( - TASKS_COLLECTION_NAME - ), + dittoService.getDitto().getStore().execute( + "UPDATE %s SET title = :title WHERE _id = :taskId".formatted(TASKS_COLLECTION_NAME), DittoCborSerializable.Dictionary.buildDictionary() - .put("title", newTitle) - .put("taskId", taskId) - .build() - ) - .toCompletableFuture() - .join(); - result.close(); - } catch (Error| DittoException e) { + .put("title", newTitle) + .put("taskId", taskId) + .build() + ).toCompletableFuture().join(); + } catch (Error e) { throw new RuntimeException(e); } } @Nonnull public Flux> observeAll() { - final String subscriptionQuery = - "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); + final String subscriptionQuery = "SELECT * FROM %s WHERE NOT deleted".formatted(TASKS_COLLECTION_NAME); final String displayQuery = subscriptionQuery + " ORDER BY title ASC"; - return Flux.create( - emitter -> { - Ditto ditto = dittoService.getDitto(); - try { - @SuppressWarnings("resource") - DittoSyncSubscription subscription = ditto - .getSync() - .registerSubscription(subscriptionQuery); - - @SuppressWarnings("resource") - DittoStoreObserver observer = ditto - .getStore() - .registerObserver(displayQuery, results -> - emitter.next( - results - .getItems() - .stream() - .map(item -> { - try { - return this.itemToTask(item); - } catch (Exception e) { - emitter.error(e); - throw new RuntimeException(e); - } - }) - .toList() - ) - ); - - emitter.onDispose(() -> { - // TODO: Can't just catch, this potentially leaks the `observer` resource. - try { - subscription.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - try { - observer.close(); - } catch (DittoException e) { - throw new RuntimeException(e); - } - }); - } catch (DittoException e) { - emitter.error(e); - } - }, - FluxSink.OverflowStrategy.LATEST - ); + return Flux.create(emitter -> { + Ditto ditto = dittoService.getDitto(); + try { + DittoSyncSubscription subscription = ditto.getSync().registerSubscription(subscriptionQuery); + DittoStoreObserver observer = ditto.getStore().registerObserver(displayQuery, results -> + emitter.next(results.getItems().stream().map(this::itemToTask).toList())); + + emitter.onDispose(() -> { + // TODO: Can't just catch, this potentially leaks the `observer` resource. + try { + subscription.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + try { + observer.close(); + } catch (DittoException e) { + throw new RuntimeException(e); + } + }); + } catch (DittoException e) { + emitter.error(e); + } + }, FluxSink.OverflowStrategy.LATEST); } - private Task itemToTask(@Nonnull DittoQueryResultItem item) throws DittoException { + private Task itemToTask(@Nonnull DittoQueryResultItem item) { DittoCborSerializable.Dictionary value = item.getValue(); try { return new Task( From 49eb30bea9cd9d8789ca29303f0ab170a27d759d Mon Sep 17 00:00:00 2001 From: Ben Chatelain Date: Thu, 11 Dec 2025 12:29:02 -0700 Subject: [PATCH 17/17] refactor: disable custom persistenceDirectory --- .../ditto/example/spring/quickstart/service/DittoService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java index d4a5ebf36..fa0400a2b 100644 --- a/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java +++ b/java-server/src/main/java/com/ditto/example/spring/quickstart/service/DittoService.java @@ -45,9 +45,8 @@ public class DittoService implements DisposableBean { * Setup Ditto Config * https://docs.ditto.live/sdk/latest/install-guides/java#integrating-and-initializing */ - DittoConfig dittoConfig = new DittoConfig.Builder(DittoSecretsConfiguration.DITTO_APP_ID) - .persistenceDirectory("C:\\ditto-quickstart") +// .persistenceDirectory("/tmp/ditto-quickstart") .serverConnect(DittoSecretsConfiguration.DITTO_AUTH_URL) .build();