blob: 376b5d5bc8742afff1bdee6be7e900521b9c6c95 [file] [log] [blame] [view]
# API Guide
This document describes how to use Ledger for synchronized application storage
in a client application. The goal of the document is to explain the key concepts
and the most common operations, not necessarily to cover the entire API surface.
[TOC]
## Connecting to Ledger
The Ledger API is exposed through a [FIDL interface].
Access to Ledger is vended from the `fuchsia::modular::ComponentContext` of the running
application. Each application component has a separate private data store for
each user on behalf of which it runs.
|||---|||
#### C++
``` cpp
module_context->GetComponentContext(
component_context_.NewRequest());
fuchsia::ledger::LedgerPtr ledger;
ledger.set_error_handler([](zx_status_t status) {
// Handle ledger and connection errors.
});
component_context_->GetLedger(ledger.NewRequest());
```
#### Dart
``` dart
moduleContext.getComponentContext(
componentContext.ctrl.request());
final LedgerProxy ledger = new LedgerProxy();
ledger.ctrl.error.then((ProxyError error) {
// Handle ledger and connection errors.
});
componentContext.getLedger(ledger.ctrl.request());
```
|||---|||
*** aside
Note that the Ledger connection is provided through a FIDL *interface request*,
so it can be immediately used to make requests.
***
## Working with pages
Data stored in Ledger is grouped into separate independent key-value stores
called *pages*.
Pages are identified by ids of 16 bytes each. When creating a page, the
application can pick its id (for example, to create/access a page with a name
that is fixed and known in advance), or can request a new unique id to be
generated by Ledger (for example, to create pages that correspond 1-1 to
business logic objects). Ledger also offers a convenience method `GetRootPage()`
which retrieves the page with id `[0, 0 ... , 0]`.
|||---|||
#### C++
``` cpp
fuchsia::ledger::PagePtr page;
ledger->GetRootPage(page.NewRequest(),
[] (fuchsia::ledger::Status status) {
if (status != fuchsia::ledger::Status::OK) {
// Handle errors.
}
});
page.set_error_handler([] (zx_status_t status) {
// Handle connection errors.
});
```
#### Dart
``` dart
final PageProxy page = new PageProxy();
_ledger.getRootPage(
page.ctrl.request(), (Status status) {
if (status != Status.ok) {
// Handle errors.
}
});
page.ctrl.error.then((ProxyError error) {
// Handle connection errors.
}
```
|||---|||
## Page API
Each page exposes a key-value store API.
The state of each page changes in commits which form a DAG. This DAG is not
directly exposed to the developer through the Ledger API, but we mention it as
it helps to understand how the page operations described below behave.
*** promo
History of a page can and does diverge when the page is modified concurrently on
multiple devices (or even locally on separate connections to the same page).
However, it's important to note that any given local page connection (/page
proxy) always tracks **one path** through the commit history and only advances
forward.
***
### Writing
Values can be written/modified using mutation methods of the Page interface.
There are two ways to write a value into a page:
- directly, passing the value in a FIDL message (that's `Put()` and
`PutWithPriority()` methods). This is handy, but note that the developer
needs to ensure that the value does not exceed the maximum payload size of a
FIDL message.
- in two steps: first streaming the data to be written to a "reference", and
then setting the value using the reference. (through a combination of
`CreateReferenceFromSocket()`, `CreateReferenceFromBuffer()` and
`PutReference()`. Note that the created reference is temporary and not
persisted through app restarts - it must be set through `PutReference()`, or
discarded.
### Reading
In order to ensure that reads across multiple entries (key-value pairs) are
consistent, read operations are exposed through a snapshot API. Snapshot of the
page can be obtained:
- directly from the page interface (`GetSnapshot()`), yielding a snapshot
corresponding to the state of the page tracked by this page connection
- through the [watcher](#Watch) interface, which when delivering each
notification allows to also request the snapshot corresponding to the state
associated with the notification
Once the snapshot is obtained, data can be read using the `GetEntries()` method
which retrieves multiple entries, or the `Get()` method which retrieves the
value associated with the particular key.
#### Range queries
The `GetEntries()` method takes a `key_start` argument, allowing the app to
perform a *range query*. In order to retrieve all entries between two keys, we
need to call `GetEntries()` with the first key passed as `key_start`, and
continue reading the paginated response as long as the returned values are in
the desired range.
### Lazy values
`PutWithPriority()` and `PutReference()` methods allow the app to write a value
as a *lazy value*. Compared with the default *eager values*, lazy values are not
guaranteed to be proactively fetched to the local device when the Page syncs to
the state that contains them. The **keys** of the lazy values are always synced
eagerly, but the value itself might be missing.
To ensure predictable performance of read operations, `Get()` and `GetEntries`
methods never retrieve the missing lazy values from the network - these must be
explicitly requested through a dedicated `Fetch()` method.
### Watch
The client application can register a watcher to be notified of changes to the
state tracked by the local page connection. As typically we are interested in
retrieving the initial base state at the moment of registering the watcher,
the watchers are registered using the `GetSnapshot()` method.
[C++ watcher example], [Dart watcher example].
`GetSnapshot()` takes an optional `key_prefix` parameter, which allows the
client app to register specifically for change notifications within a particular
prefix of keys.
### Transactions
Transactions allow the application to make a set of changes that are guaranteed
to be synced and surfaced atomically. A client application would typically:
- start a transaction
- get a snapshot of the page state through `GetSnapshot()` or a watcher (see
below), and read the data
- make changes through the Page interface
- commit the transaction
Once a transaction is started, the state of the page tracked on the page
connection is pinned and won't advance until the transaction is either committed
or aborted - this ensures that the transaction writes affect precisely the state
visible on the page snapshot.
There can be only one transaction in progress per `Page` connection, but
clients can create multiple connections to the same page. Each of these
connections can run a transaction independently from the others.
*** note
*Watch and transactions*: As starting a transaction pins the state of the page
visible on the particular page connection, the client app won't receive any
watch notifications (for watchers registered on this page connection) while the
transaction is in progress.
Conversely, on a page connection with registered watchers, a
`StartTransaction()` call will only return when the app finishes processing all
pending change notification. This ensures the app knows the base state of the
page when performing a transaction.
***
## Conflict resolution
If the page history has diverged and has more than one head, conflict resolution
will be invoked to merge the heads.
*** aside
Note that this condition is not associated with any particular local page
connection - there could be multiple local page connections on different
divergent heads. Conflict resolution is therefore configured through the Ledger
connection.
***
By default, a key-by-key last-one-wins policy is used to merge divergent
commits, based on best-effort, not-guaranteed-to-be-right, timestamps of each
commit.
The client might opt for a different merge policy by implementing the
`ConflictResolverFactory` interface and setting it through a Ledger connection.
Available conflict resolution policies:
- last-one-wins (the default): `LAST_ONE_WINS`
For each entry modified on both sides, `LAST_ONE_WINS` takes the entry from
the most recent commit; for each entry modified only on one side (e.g.:
when adding a new key, or when modifying an existing one), the modification
is carried over. The most recent commit is determined using timestamps
assigned to the commits by the devices that created them. Note, however that
these timestamps are not guaranteed to be accurate.
- custom, invoked only for conflicting (modified on both branches being
merged) entries: `AUTOMATIC_WITH_FALLBACK`
For all modified keys, if either they are modified only on one side, or
changed in the same way (added/updated with the same value, or deleted) in
both sides, `AUTOMATIC_WITH_FALLBACK` applies the change automatically. If,
however any key is modified on both sides with a non identical change, the
app gets a callback with all changed entries from both sides (not just the
conflicting ones).
- custom, invoked for all merges (even if the entries modified by each branch
are non-conflicting): `CUSTOM`
Conflicting changes are ignored and there are no merges performed.
See also [Conflict Resolution](conflict_resolution.md).
## Synchronization
Synchronization is configured at the Ledger level as described in the [user
guide](user_guide.md). Once configured, the Ledger will upload local changes,
and download changes made by other devices. These operations will happen
automatically in the background, and clients do not have to manage them.
## See also
- [Data Organization](data_organization.md)
- [Examples of client apps](examples.md)
[FIDL interface]: /public/fidl/fuchsia.ledger/ledger.fidl
[C++ watcher example]: /examples/todo_cpp/todo.h
[Dart watcher example]: https://fuchsia.googlesource.com/topaz/+/master/examples/ledger/todo_list/lib/src/models/todo_list_model.dart