{"id":2929,"date":"2017-03-09T23:13:48","date_gmt":"2017-03-10T07:13:48","guid":{"rendered":"https:\/\/www.couchbase.com\/blog\/?p=2929"},"modified":"2025-06-13T19:58:39","modified_gmt":"2025-06-14T02:58:39","slug":"couchbase-mobile-changes-explorer-part-2","status":"publish","type":"post","link":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/","title":{"rendered":"Couchbase Mobile Changes Explorer \u2013 Part. 2"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>The <a href=\"https:\/\/www.couchbase.com\/developers\/mobile\/?utm_source=blogs&amp;utm_medium=link&amp;utm_campaign=blogs\">Couchbase Mobile<\/a> <a href=\"https:\/\/developer.couchbase.com\/documentation\/mobile\/current\/guides\/sync-gateway\/index.html?utm_source=blogs&amp;utm_medium=link&amp;utm_campaign=blogs\">Sync Gateway<\/a> changes feed provides a way to monitor events in a mobile deployment. The feed makes it feasible to write sophisticated business logic. I wrote a tool to help examine and understand the feed. You can read an introduction and description in <a href=\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-1\/\">part one<\/a> of this two part series. The code also serves as an example of listening to the feed.<\/p>\n<h2>The code<\/h2>\n<p>I&#8217;ve included the major classes from the app code here. This is the first version, so it can use plenty of enhancements. The parameters are all hard-wired. Check the project <a href=\"https:\/\/github.com\/couchbaselabs\/CBM-Changes-Explorer\">here on GitHub<\/a> for updates. \u00a0You can also find instructions for building, running, and packaging the app there.<\/p>\n<h3>JavaFX: The Controller class<\/h3>\n<p>JavaFX breaks simple apps into a controller class and a declarative UI. Let&#8217;s walk through the controller.<\/p>\n<pre class=\"lang:java decode:true \">package com.couchbase.mobile;\r\n\r\nimport com.couchbase.lite.*;\r\nimport com.fasterxml.jackson.core.JsonProcessingException;\r\nimport javafx.application.Platform;\r\nimport javafx.beans.value.ChangeListener;\r\nimport javafx.beans.value.ObservableValue;\r\nimport javafx.collections.FXCollections;\r\nimport javafx.collections.ObservableList;\r\nimport javafx.event.ActionEvent;\r\nimport javafx.fxml.FXML;\r\nimport javafx.scene.control.*;\r\nimport javafx.scene.control.TextField;\r\n\r\nimport java.io.IOException;\r\nimport java.net.MalformedURLException;\r\nimport java.net.URL;\r\nimport java.util.Map;\r\n\r\nimport static com.couchbase.mobile.Runtime.mapper;\r\n\r\npublic class Controller implements LiveQuery.ChangeListener, ChangeListener,\r\n    SGMonitor.ChangesFeedListener, DBService.ReplicationStateListener {\r\n  private static final String SYNC_GATEWAY_HOST = \"https:\/\/localhost\";\r\n  private static final String SG_PUBLIC_URL = SYNC_GATEWAY_HOST + \":4984\/\" + DBService.DATABASE;\r\n  private static final String SG_ADMIN_URL = SYNC_GATEWAY_HOST + \":4985\/\" + DBService.DATABASE;\r\n\r\n  private static final String TOGGLE_INACTIVE = \"-fx-background-color: #e6555d;\";\r\n  private static final String TOGGLE_ACTIVE = \"-fx-background-color: #ade6a6;\";\r\n  private static final String TOGGLE_DISABLED = \"-fx-background-color: #555555;\";\r\n\r\n  @FXML private ListView documentList;\r\n  private ObservableList documents = FXCollections.observableArrayList();\r\n  @FXML private TextArea contentsText;\r\n  @FXML private TextArea changesFeed;\r\n  @FXML private TextField usernameText;\r\n  @FXML private TextField passwordText;\r\n  @FXML private ToggleButton applyCredentialsBtn;\r\n  @FXML private ToggleButton syncBtn;\r\n\r\n  private DBService service = DBService.getInstance();\r\n  private Database db = service.getDatabase();\r\n  private SGMonitor changesMonitor;\r\n  private LiveQuery liveQuery;\r\n<\/pre>\n<p>This first listing shows a bunch of boiler plate code. I implement several listeners for the UI within the class itself to cut down of files. This is for illustration purposes.<\/p>\n<p>The <code>@FXML<\/code> annotations mark all the fields that the framework will automatically bind to portions of the UI.<\/p>\n<p>Next comes initialization. JavaFX calls this method as part of its standard lifecycle.<\/p>\n<pre class=\"lang:java decode:true \">  @FXML private void initialize() {\r\n    documentListInitialize();\r\n    documentList.setItems(documents);\r\n\r\n    setState(applyCredentialsBtn, false);\r\n    setState(syncBtn, false);\r\n\r\n    service.addReplicationStateListener(this);\r\n\r\n    changesMonitor = new SGMonitor(SG_ADMIN_URL, \"false\", \"true\", \"0\", \"all_docs\", this);\r\n    changesMonitor.start();\r\n  }\r\n\r\n  private void documentListInitialize() {\r\n    Query query = db.createAllDocumentsQuery();\r\n    query.setAllDocsMode(Query.AllDocsMode.INCLUDE_DELETED);\r\n    liveQuery = query.toLiveQuery();\r\n    liveQuery.addChangeListener(this);\r\n    liveQuery.start();\r\n\r\n    documentList.getSelectionModel().selectedItemProperty().addListener(this);\r\n  }<\/pre>\n<p>I&#8217;ve broken out the document list initialization into its own routine. The document list gets bound to the <code>documentList<\/code> variable. In turn <code>documentList<\/code> will update the UI whenever the item list we pass in changes.<\/p>\n<p>I set up a live query to monitor the client database for any changes. This happens through an &#8220;all docs&#8221; query. An all docs query doesn&#8217;t require an associated view. I set <code>INCLUDE_DELETED<\/code> so the tool can show what a deleted document looks like in the database.<\/p>\n<p>With the other bindings in place, we just have to update the <code>documents<\/code> list. We&#8217;ll see the live query listener that does that further along.<\/p>\n<p>The next few lines set the initial state of a couple of toggle buttons. I need an extra listener to keep the <code>Sync<\/code> button consistent with the actual state of the replications. More on this further along in the article.<\/p>\n<p>I wrote a separate class to monitor Sync Gateway. The initialization code finished by creating a new monitor instance and kicking it off.<\/p>\n<p>The next section contains several listeners.<\/p>\n<pre class=\"lang:java decode:true \">  \/\/ LiveQuery.ChangeListener\r\n  @Override\r\n  public void changed(LiveQuery.ChangeEvent event) {\r\n    if (event.getSource().equals(liveQuery)) {\r\n      Platform.runLater(() -&gt; {\r\n        QueryEnumerator rows = event.getRows();\r\n\r\n        documents.clear();\r\n\r\n        rows.forEach(queryRow -&gt; documents.add(queryRow.getDocumentId()));\r\n      });\r\n    }\r\n  }<\/pre>\n<p>Here&#8217;s the live query listener that gets called whenever the local database changes. I didn&#8217;t design the tool for working with massive databases. So, whenever the data changes, I just took the brute force approach of rereading every document. The <code>getRows<\/code> method returns an enumerator that will index doing just that. JavaFX takes care of updating the UI when <code>documents<\/code> changes.<\/p>\n<pre class=\"lang:java decode:true \">  \/\/ ListView ChangeListener\r\n  @Override\r\n  public void changed(ObservableValue observable, String oldId, String newId) {\r\n    if (null == newId) return;\r\n\r\n    Map properties = db.getDocument(newId).getProperties();\r\n\r\n    try {\r\n      String json = mapper.writeValueAsString(properties);\r\n\r\n      contentsText.setText(prettyText(json));\r\n    } catch (JsonProcessingException ex) {\r\n      ex.printStackTrace();\r\n      Dialog.display(ex);\r\n    }\r\n  }<\/pre>\n<p>This listener takes care of tracking when a user clicks on an entry in the document list. The entries are the document IDs, so we can use a selection to pull the document directly from the database.<\/p>\n<pre class=\"lang:java decode:true \">  \/\/ SGMonitor.ChangesFeedListener\r\n  @Override\r\n  public void onResponse(String body) {\r\n    changesFeed.appendText(prettyText((String) body));\r\n  }<\/pre>\n<p>I used a callback approach to get the results of the changes feed. The interface is defined in the <code>SGMonitor<\/code> class. It has just the one method. In this implementation I simply take the body of the feed response and tack it on to the existing text in the changes feed text pane. There&#8217;s a little formatting done to make it easier to read, too.<\/p>\n<pre class=\"lang:java decode:true \">  \/\/ DBService.ReplicationStateListener\r\n  @Override\r\n  public void onChange(boolean isActive) {\r\n    setState(syncBtn, isActive);\r\n  }<\/pre>\n<p>Finally I added a listener for replication activity. The interface comes from the DBService helper class. I wrote a bit about detecting the state of a replication <a href=\"https:\/\/www.couchbase.com\/blog\/determining-status-replication-couchbase-lite\/\">here<\/a>. For this app I just need to know whether a replication is running or not to keep the <code>Sync<\/code> button state consistent. This handles cases where a user tries to start a sync but it fails. This can happen if they need to provide authentication credentials but haven&#8217;t, for example.<\/p>\n<p>Next we have several methods bound to UI elements. JavaFX handles much of the wiring.<\/p>\n<pre class=\"lang:java decode:true \">  @FXML private void applyCredentialsToggled(ActionEvent event) {\r\n    String username = null;\r\n    String password = null;\r\n\r\n    if (applyCredentialsBtn.isSelected()) {\r\n      username = usernameText.getText();\r\n      password = passwordText.getText();\r\n    }\r\n\r\n    DBService.getInstance().setCredentials(username, password);\r\n    applyCredentialsBtn.setStyle(applyCredentialsBtn.isSelected() ? TOGGLE_ACTIVE : TOGGLE_INACTIVE);\r\n  }<\/pre>\n<p>Here I set the use of authentication credentials whenever the corresponding button gets toggled.<\/p>\n<pre class=\"lang:java decode:true \">  @FXML private void saveContentsClicked(ActionEvent event) {\r\n    Map properties = null;\r\n    Document document;\r\n\r\n    try {\r\n      properties = mapper.readValue(contentsText.getText(), Map.class);\r\n    } catch (IOException ex) {\r\n      ex.printStackTrace();\r\n      Dialog.display(ex);\r\n    }\r\n\r\n    if (properties.containsKey(\"_id\")) {\r\n      document = db.getDocument((String) properties.get(\"_id\"));\r\n    } else {\r\n      document = db.createDocument();\r\n    }\r\n\r\n    try {\r\n      document.putProperties(properties);\r\n    } catch (CouchbaseLiteException ex) {\r\n      ex.printStackTrace();\r\n      Dialog.display(ex);\r\n    }\r\n  }\r\n<\/pre>\n<p>This code shows a couple of interesting items. I use a Jackson <code>ObjectMapper<\/code> instance to convert the text in the content pane to a property map.<\/p>\n<p>Next I check for an entry <code>_id<\/code>. Couchbase Mobile reserves most properties starting with an &#8220;_&#8221; for system use (with special exceptions). If the text we&#8217;re trying to convert contains <code>_id<\/code>, I assume this is an edit to an existing document. Otherwise I create a new document.<\/p>\n<p>So, in a nutshell, we have an example of both creating and updating documents. This isn&#8217;t the preferred way to update, although it suffices in many cases. You can read more about updates <a href=\"https:\/\/www.couchbase.com\/blog\/better-updates-couchbase-lite\/\">here<\/a>.<\/p>\n<pre><code class=\"language-java\">  @FXML private void syncToggled(ActionEvent event) {\r\n    try {\r\n      syncBtn.setDisable(true);\r\n      syncBtn.setStyle(TOGGLE_DISABLED);\r\n      service.toggleReplication(new URL(SG_PUBLIC_URL), true);\r\n    } catch (Exception ex) {\r\n      ex.printStackTrace();\r\n      Dialog.display(ex);\r\n      syncBtn.setDisable(false);\r\n    }\r\n  }<\/code><\/pre>\n<p>This reacts to toggling the <code>Sync<\/code> button. Recall though that we use a listener to verify the state elsewhere.<\/p>\n<pre class=\"lang:java decode:true \">  @FXML private void exitClicked(ActionEvent event) {\r\n    \/\/ Try to shut everything down gracefully\r\n    changesMonitor.stop();\r\n    liveQuery.stop();\r\n    service.stopReplication();\r\n    db.close();\r\n    db.getManager().close();\r\n\r\n    Platform.exit();\r\n  }\r\n\r\n  private void setState(ToggleButton btn, boolean active) {\r\n    btn.setSelected(active);\r\n    btn.setStyle(active ? TOGGLE_ACTIVE : TOGGLE_INACTIVE);\r\n    btn.setDisable(false);\r\n  }\r\n\r\n  private String prettyText(String json) {\r\n    String out = null;\r\n\r\n    try {\r\n      Object object = mapper.readValue(json, Object.class);\r\n\r\n      out = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);\r\n    } catch (Exception ex) {\r\n      ex.printStackTrace();\r\n    }\r\n\r\n    return out;\r\n  }\r\n}\r\n<\/pre>\n<p>The rest of the code here are just helper bits and a piece to shutdown everything before exiting.<\/p>\n<h3>The Database Helper class<\/h3>\n<p>This shows the code for a straight-forward database helper class. For the most part I just find this class a nice packaging of the typical operations needed for managing a database and starting a standard bidirectional set of replications. I&#8217;m including it here because I find it useful and for clarity.<\/p>\n<p>I do implement the <code>Replication.ChangeListener<\/code> interface. That&#8217;s maybe a little unusual. I mentioned the reason earlier on. This link takes you to the <a href=\"https:\/\/www.couchbase.com\/blog\/determining-status-replication-couchbase-lite\/\">blog post<\/a> about it.<\/p>\n<pre class=\"lang:java decode:true \">package com.couchbase.mobile;\r\n\r\nimport com.couchbase.lite.Database;\r\nimport com.couchbase.lite.JavaContext;\r\nimport com.couchbase.lite.Manager;\r\nimport com.couchbase.lite.auth.Authenticator;\r\nimport com.couchbase.lite.auth.AuthenticatorFactory;\r\nimport com.couchbase.lite.replicator.Replication;\r\nimport com.couchbase.lite.replicator.ReplicationState;\r\n\r\nimport java.net.URL;\r\nimport java.util.ArrayList;\r\nimport java.util.List;\r\n\r\npublic class DBService implements Replication.ChangeListener {\r\n  public static final String DATABASE = \"db\";\r\n\r\n  private static final String DB_DIRECTORY = \"data\";\r\n\r\n  private Manager manager;\r\n  private Database database;\r\n  private Replication pushReplication = null;\r\n  private Replication pullReplication = null;\r\n  private boolean replicationActive = false;\r\n  private List stateListeners = new ArrayList();\r\n  private String username = null;\r\n  private String password = null;\r\n\r\n  private DBService() {\r\n    try {\r\n      manager = new Manager(new JavaContext(DB_DIRECTORY), Manager.DEFAULT_OPTIONS);\r\n      database = manager.getDatabase(DATABASE);\r\n    } catch (Exception ex) {\r\n      ex.printStackTrace();\r\n    }\r\n  }\r\n\r\n  private static class Holder {\r\n    private static DBService INSTANCE = new DBService();\r\n  }\r\n\r\n\r\n  public interface ReplicationStateListener {\r\n    void onChange(boolean isActive);\r\n  }\r\n\r\n  public static DBService getInstance() {\r\n    return Holder.INSTANCE;\r\n  }\r\n\r\n  public Database getDatabase() {\r\n    return database;\r\n  }\r\n\r\n  public void setCredentials(String username, String password) {\r\n    this.username = username;\r\n    this.password = password;\r\n  }\r\n\r\n  public void toggleReplication(URL gateway, boolean continuous) {\r\n    if (replicationActive) {\r\n      stopReplication();\r\n    } else {\r\n      startReplication(gateway, continuous);\r\n    }\r\n  }\r\n\r\n  public void startReplication(URL gateway, boolean continuous) {\r\n    if (replicationActive) {\r\n      stopReplication();\r\n    }\r\n\r\n    pushReplication = database.createPushReplication(gateway);\r\n    pullReplication = database.createPullReplication(gateway);\r\n    pushReplication.setContinuous(continuous);\r\n    pullReplication.setContinuous(continuous);\r\n\r\n    if (username != null) {\r\n      Authenticator auth = AuthenticatorFactory.createBasicAuthenticator(username, password);\r\n      pushReplication.setAuthenticator(auth);\r\n      pullReplication.setAuthenticator(auth);\r\n    }\r\n\r\n    pushReplication.addChangeListener(this);\r\n    pullReplication.addChangeListener(this);\r\n\r\n    pushReplication.start();\r\n    pullReplication.start();\r\n  }\r\n\r\n  public void stopReplication() {\r\n    if (!replicationActive) return;\r\n\r\n    pushReplication.stop();\r\n    pullReplication.stop();\r\n\r\n    pushReplication = null;\r\n    pullReplication = null;\r\n  }\r\n\r\n  public void addReplicationStateListener(ReplicationStateListener listener) {\r\n    stateListeners.add(listener);\r\n  }\r\n\r\n  public void removeReplicationStateListener(ReplicationStateListener listener) {\r\n    stateListeners.remove(listener);\r\n  }\r\n\r\n  \/\/ Replication.ChangeListener\r\n  @Override\r\n  public void changed(Replication.ChangeEvent changeEvent) {\r\n    if (changeEvent.getError() != null) {\r\n      Throwable lastError = changeEvent.getError();\r\n\r\n      Dialog.display(lastError.getMessage());\r\n\r\n      return;\r\n    }\r\n\r\n    if (changeEvent.getTransition() == null) return;\r\n\r\n    ReplicationState dest = changeEvent.getTransition().getDestination();\r\n\r\n    replicationActive = ((dest == ReplicationState.STOPPING || dest == ReplicationState.STOPPED) ? false : true);\r\n\r\n    stateListeners.forEach(listener -&gt; listener.onChange(replicationActive));\r\n  }\r\n}<\/pre>\n<h3>The Sync Gateway Monitor class<\/h3>\n<p>Finally, let&#8217;s take a look at the helper class for monitoring Sync Gateway. I&#8217;ll walk through this in pieces, too.<\/p>\n<pre class=\"lang:java decode:true \">package com.couchbase.mobile;\r\n\r\nimport com.fasterxml.jackson.databind.JsonNode;\r\nimport okhttp3.*;\r\n\r\nimport java.io.IOException;\r\nimport java.net.SocketException;\r\nimport java.util.concurrent.TimeUnit;\r\n\r\nimport static com.couchbase.mobile.Runtime.mapper;\r\n\r\npublic class SGMonitor {\r\n  private static final OkHttpClient client = new OkHttpClient.Builder()\r\n      .readTimeout(1, TimeUnit.DAYS)\r\n      .build();\r\n  private ChangesFeedListener listener;\r\n  private HttpUrl.Builder urlBuilder;\r\n  private Thread monitorThread;\r\n  private String since = \"0\";\r\n  private Call call;\r\n\r\n  SGMonitor(String url, String activeOnly, String includeDocs, String since, String style,\r\n            ChangesFeedListener listener) {\r\n    this.since = since;\r\n\r\n    urlBuilder = HttpUrl.parse(url).newBuilder()\r\n        .addPathSegment(\"_changes\")\r\n        .addQueryParameter(\"active_only\", activeOnly)\r\n        .addQueryParameter(\"include_docs\", includeDocs)\r\n        .addQueryParameter(\"style\", style)\r\n        .addQueryParameter(\"since\", since)\r\n        .addQueryParameter(\"feed\", \"longpoll\")\r\n        .addQueryParameter(\"timeout\", \"0\");\r\n\r\n    this.listener = listener;\r\n  }<\/pre>\n<p>I use the <a href=\"https:\/\/square.github.io\/okhttp\/\">OkHttp library from Square<\/a>. Currently Couchbase Lite uses this library too, internally. OkHttp uses a builder pattern. I prepare a builder instance I&#8217;ll use through the rest of the code in the class constructor. You can read about the meaning of all the parameters in the <a href=\"https:\/\/developer.couchbase.com\/documentation\/mobile\/current\/references\/sync-gateway\/index.html\">Sync Gateway documentation<\/a>.<\/p>\n<pre><code class=\"language-java\">  public interface ChangesFeedListener {\r\n    void onResponse(String body);\r\n  }\r\n\r\n  public void start() {\r\n    monitorThread = new Thread(() -&gt; {\r\n      while (!Thread.interrupted()) {\r\n        Request request = new Request.Builder()\r\n            .url(urlBuilder.build())\r\n            .build();\r\n\r\n        call = client.newCall(request);\r\n\r\n        try (Response response = call.execute()) {\r\n          if (!response.isSuccessful()) throw new IOException(\"Unexpected code \" + response);\r\n\r\n          String body = response.body().string();\r\n\r\n          JsonNode tree = mapper.readTree(body);\r\n\r\n          since = tree.get(\"last_seq\").asText();\r\n          urlBuilder.setQueryParameter(\"since\", since);\r\n\r\n          listener.onResponse(body);\r\n        } catch (SocketException ex) {\r\n          return;\r\n        } catch (IOException ex) {\r\n          ex.printStackTrace();\r\n          Dialog.display(ex);\r\n        }\r\n      }\r\n    });\r\n\r\n    monitorThread.setDaemon(true);\r\n    monitorThread.start();\r\n  }<\/code><\/pre>\n<p>The <code>start<\/code> method has the most interesting part of the code. It spins up a background thread. Underneath the thread setup and control code I run a continuous loop. The loop does synchronous network calls. The error handling is simple. Just throw an exception if anything goes wrong.<\/p>\n<p>Sync Gateway responds with JSON strings. You can see the code pulls apart the response and parses the JSON into a <code>JsonNode<\/code> object. This is all to get at the <code>last_seq<\/code> value in the response.<\/p>\n<p>In order to track what to send next, the changes feed relies on a simple sequence mechanism. You should treat this as an opaque object. Take the value of <code>last_seq<\/code> from the previous response, and set the <code>since<\/code> parameter to that same value for the next request.<\/p>\n<p>There&#8217;s no real harm in not supplying the <code>since<\/code> parameter. Sync Gateway will just replay all changes from the start if it&#8217;s missing. That&#8217;s why you&#8217;ll see in this example, I cheat a little and always create the class instance with <code>since<\/code> set to the string &#8220;0&#8221;.<\/p>\n<p>In a real world application, you might want to have some way to save the last sequence string your app has processed, rather than churning through the change history every time.<\/p>\n<p>The rest of the code is just a couple of short methods.<\/p>\n<pre class=\"lang:java decode:true \">  public void stop() {\r\n    monitorThread.interrupt();\r\n    call.cancel();\r\n  }\r\n\r\n  public String getSince() {\r\n    return since;\r\n  }\r\n}<\/pre>\n<p>And that&#8217;s it for the main classes. There are others needed for the complete app.<\/p>\n<p>Check out the <a href=\"https:\/\/github.com\/couchbaselabs\/CBM-Changes-Explorer\">GitHub repo<\/a> to see all the code and instructions to build it.<\/p>\n<p>Read a discussion of the app and how to use it in <a href=\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-1\/\">part one<\/a>.<\/p>\n<h2>Postscript<\/h2>\n<p>You can find more resources on our <a href=\"https:\/\/www.couchbase.com\/developers\/community\/?utm_source=blogs&amp;utm_medium=link&amp;utm_campaign=blogs\">developer portal<\/a> and follow us on Twitter <a href=\"https:\/\/twitter.com\/CouchbaseDev\">@CouchbaseDev<\/a>.<\/p>\n<p>You can post questions on our <a href=\"https:\/\/www.couchbase.com\/forums\/?utm_source=blogs&amp;utm_medium=link&amp;utm_campaign=blogs\">forums<\/a>. And we actively participate on <a href=\"https:\/\/stackoverflow.com\/questions\/tagged\/couchbase\">Stack Overflow<\/a>.<\/p>\n<p>Hit me up on Twitter with any questions, comments, topics you&#8217;d like to see, etc. <a href=\"https:\/\/twitter.com\/HodGreeley\">@HodGreeley<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction The Couchbase Mobile Sync Gateway changes feed provides a way to monitor events in a mobile deployment. The feed makes it feasible to write sophisticated business logic. I wrote a tool to help examine and understand the feed. You [&hellip;]<\/p>\n","protected":false},"author":73,"featured_media":2888,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[1810,1818,2366],"tags":[1867,1868,1809],"ppma_author":[9042],"class_list":["post-2929","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-couchbase-mobile","category-java","category-sync-gateway","tag-business-logic","tag-changes-feed","tag-document"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v26.0 (Yoast SEO v26.0) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Couchbase Mobile Changes Explorer \u2013 Part. 2 - The Couchbase Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Couchbase Mobile Changes Explorer \u2013 Part. 2\" \/>\n<meta property=\"og:description\" content=\"Introduction The Couchbase Mobile Sync Gateway changes feed provides a way to monitor events in a mobile deployment. The feed makes it feasible to write sophisticated business logic. I wrote a tool to help examine and understand the feed. You [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/\" \/>\n<meta property=\"og:site_name\" content=\"The Couchbase Blog\" \/>\n<meta property=\"article:published_time\" content=\"2017-03-10T07:13:48+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-06-14T02:58:39+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2017\/02\/ChangesExplorer.gif\" \/>\n\t<meta property=\"og:image:width\" content=\"480\" \/>\n\t<meta property=\"og:image:height\" content=\"275\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/gif\" \/>\n<meta name=\"author\" content=\"Hod Greeley, Developer Advocate, Couchbase\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@HodGreeley\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Hod Greeley, Developer Advocate, Couchbase\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/\"},\"author\":{\"name\":\"Hod Greeley, Developer Advocate, Couchbase\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/9b62593c8a13531e53d52fcd5aabbca4\"},\"headline\":\"Couchbase Mobile Changes Explorer \u2013 Part. 2\",\"datePublished\":\"2017-03-10T07:13:48+00:00\",\"dateModified\":\"2025-06-14T02:58:39+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/\"},\"wordCount\":1261,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2017\/02\/ChangesExplorer.gif\",\"keywords\":[\"Business Logic\",\"Changes Feed\",\"document\"],\"articleSection\":[\"Couchbase Mobile\",\"Java\",\"Sync Gateway\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/\",\"url\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/\",\"name\":\"Couchbase Mobile Changes Explorer \u2013 Part. 2 - The Couchbase Blog\",\"isPartOf\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2017\/02\/ChangesExplorer.gif\",\"datePublished\":\"2017-03-10T07:13:48+00:00\",\"dateModified\":\"2025-06-14T02:58:39+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#primaryimage\",\"url\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2017\/02\/ChangesExplorer.gif\",\"contentUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2017\/02\/ChangesExplorer.gif\",\"width\":480,\"height\":275,\"caption\":\"Couchbase Mobile Changes Explorer Animated Gif\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.couchbase.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Couchbase Mobile Changes Explorer \u2013 Part. 2\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#website\",\"url\":\"https:\/\/www.couchbase.com\/blog\/\",\"name\":\"The Couchbase Blog\",\"description\":\"Couchbase, the NoSQL Database\",\"publisher\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.couchbase.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#organization\",\"name\":\"The Couchbase Blog\",\"url\":\"https:\/\/www.couchbase.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png\",\"contentUrl\":\"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png\",\"width\":218,\"height\":34,\"caption\":\"The Couchbase Blog\"},\"image\":{\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/9b62593c8a13531e53d52fcd5aabbca4\",\"name\":\"Hod Greeley, Developer Advocate, Couchbase\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/image\/21eb69cb5d4a401fb23b149e4f4e9e87\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/52d0018695c0ced0d1c68cf64a6195c81dbac03dce5983f98eb209e7c84350df?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/52d0018695c0ced0d1c68cf64a6195c81dbac03dce5983f98eb209e7c84350df?s=96&d=mm&r=g\",\"caption\":\"Hod Greeley, Developer Advocate, Couchbase\"},\"description\":\"Hod Greeley is a Developer Advocate for Couchbase, living in Silicon Valley. He has over two decades of experience as a software engineer and engineering manager. He has worked in a variety of software fields, including computational physics and chemistry, computer and network security, finance, and mobile. Prior to joining Couchbase in 2016, Hod led developer relations for mobile at Samsung. Hod holds a Ph.D. in chemical physics from Columbia University.\",\"sameAs\":[\"https:\/\/hod.greeley.org\/blog\",\"https:\/\/x.com\/HodGreeley\"],\"url\":\"https:\/\/www.couchbase.com\/blog\/author\/hod-greeley\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Couchbase Mobile Changes Explorer \u2013 Part. 2 - The Couchbase Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/","og_locale":"en_US","og_type":"article","og_title":"Couchbase Mobile Changes Explorer \u2013 Part. 2","og_description":"Introduction The Couchbase Mobile Sync Gateway changes feed provides a way to monitor events in a mobile deployment. The feed makes it feasible to write sophisticated business logic. I wrote a tool to help examine and understand the feed. You [&hellip;]","og_url":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/","og_site_name":"The Couchbase Blog","article_published_time":"2017-03-10T07:13:48+00:00","article_modified_time":"2025-06-14T02:58:39+00:00","og_image":[{"width":480,"height":275,"url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2017\/02\/ChangesExplorer.gif","type":"image\/gif"}],"author":"Hod Greeley, Developer Advocate, Couchbase","twitter_card":"summary_large_image","twitter_creator":"@HodGreeley","twitter_misc":{"Written by":"Hod Greeley, Developer Advocate, Couchbase","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#article","isPartOf":{"@id":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/"},"author":{"name":"Hod Greeley, Developer Advocate, Couchbase","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/9b62593c8a13531e53d52fcd5aabbca4"},"headline":"Couchbase Mobile Changes Explorer \u2013 Part. 2","datePublished":"2017-03-10T07:13:48+00:00","dateModified":"2025-06-14T02:58:39+00:00","mainEntityOfPage":{"@id":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/"},"wordCount":1261,"commentCount":0,"publisher":{"@id":"https:\/\/www.couchbase.com\/blog\/#organization"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#primaryimage"},"thumbnailUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2017\/02\/ChangesExplorer.gif","keywords":["Business Logic","Changes Feed","document"],"articleSection":["Couchbase Mobile","Java","Sync Gateway"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/","url":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/","name":"Couchbase Mobile Changes Explorer \u2013 Part. 2 - The Couchbase Blog","isPartOf":{"@id":"https:\/\/www.couchbase.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#primaryimage"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#primaryimage"},"thumbnailUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2017\/02\/ChangesExplorer.gif","datePublished":"2017-03-10T07:13:48+00:00","dateModified":"2025-06-14T02:58:39+00:00","breadcrumb":{"@id":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#primaryimage","url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2017\/02\/ChangesExplorer.gif","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/sites\/1\/2017\/02\/ChangesExplorer.gif","width":480,"height":275,"caption":"Couchbase Mobile Changes Explorer Animated Gif"},{"@type":"BreadcrumbList","@id":"https:\/\/www.couchbase.com\/blog\/couchbase-mobile-changes-explorer-part-2\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.couchbase.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Couchbase Mobile Changes Explorer \u2013 Part. 2"}]},{"@type":"WebSite","@id":"https:\/\/www.couchbase.com\/blog\/#website","url":"https:\/\/www.couchbase.com\/blog\/","name":"The Couchbase Blog","description":"Couchbase, the NoSQL Database","publisher":{"@id":"https:\/\/www.couchbase.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.couchbase.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.couchbase.com\/blog\/#organization","name":"The Couchbase Blog","url":"https:\/\/www.couchbase.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png","contentUrl":"https:\/\/www.couchbase.com\/blog\/wp-content\/uploads\/2023\/04\/admin-logo.png","width":218,"height":34,"caption":"The Couchbase Blog"},"image":{"@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/9b62593c8a13531e53d52fcd5aabbca4","name":"Hod Greeley, Developer Advocate, Couchbase","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.couchbase.com\/blog\/#\/schema\/person\/image\/21eb69cb5d4a401fb23b149e4f4e9e87","url":"https:\/\/secure.gravatar.com\/avatar\/52d0018695c0ced0d1c68cf64a6195c81dbac03dce5983f98eb209e7c84350df?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/52d0018695c0ced0d1c68cf64a6195c81dbac03dce5983f98eb209e7c84350df?s=96&d=mm&r=g","caption":"Hod Greeley, Developer Advocate, Couchbase"},"description":"Hod Greeley is a Developer Advocate for Couchbase, living in Silicon Valley. He has over two decades of experience as a software engineer and engineering manager. He has worked in a variety of software fields, including computational physics and chemistry, computer and network security, finance, and mobile. Prior to joining Couchbase in 2016, Hod led developer relations for mobile at Samsung. Hod holds a Ph.D. in chemical physics from Columbia University.","sameAs":["https:\/\/hod.greeley.org\/blog","https:\/\/x.com\/HodGreeley"],"url":"https:\/\/www.couchbase.com\/blog\/author\/hod-greeley\/"}]}},"authors":[{"term_id":9042,"user_id":73,"is_guest":0,"slug":"hod-greeley","display_name":"Hod Greeley, Developer Advocate, Couchbase","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/52d0018695c0ced0d1c68cf64a6195c81dbac03dce5983f98eb209e7c84350df?s=96&d=mm&r=g","author_category":"","last_name":"Greeley","first_name":"Hod","job_title":"","user_url":"https:\/\/hod.greeley.org\/blog","description":"Hod Greeley is a Developer Advocate for Couchbase, living in Silicon Valley. He has over two decades of experience as a software engineer and engineering manager. He has worked in a variety of software fields, including computational physics and chemistry, computer and network security, finance, and mobile. Prior to joining Couchbase in 2016, Hod led developer relations for mobile at Samsung. Hod holds a Ph.D. in chemical physics from Columbia University."}],"_links":{"self":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts\/2929","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/users\/73"}],"replies":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/comments?post=2929"}],"version-history":[{"count":0,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/posts\/2929\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/media\/2888"}],"wp:attachment":[{"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/media?parent=2929"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/categories?post=2929"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/tags?post=2929"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.couchbase.com\/blog\/wp-json\/wp\/v2\/ppma_author?post=2929"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}