-
Notifications
You must be signed in to change notification settings - Fork 145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A proposal for an IndexedDB wrapper #214
base: master
Are you sure you want to change the base?
Conversation
The example uses @Pauan 's |
"crates/worker", | ||
"crates/net", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was duplicated. I've put them in alphabetical order.
Is this API ready for review? If so, can you post what the public API looks like? Side note: I've never had to use IndexedDB so someone more knowledgeable should also review it |
Hi, sorry for the slow reply. It is ready to be reviewed for overall strategy, but some details still need ironing out, things like function/type names and error types. I will upload a document explaining design decisions are also copy its contents into this PR. |
Also pinging @c410-f3r because they have made a wrapper and probably have lots of useful thoughts/comments/suggestions. |
I've made some improvements and push latest. IMO the best way to review is to look at
|
🔔 |
Hi, sorry for the late response. I've personally never used IndexedDB so I can't comment much on the API. Can you comment what the public API looks like so it's easier for someone to review it, without having to look through the implementation? Just to clarify: I'm fine with adding support for IndexedDB in gloo-storage. I can review the implementation when the PR is ready for merge. I just want someone more knowledgeable to review the public API. |
Cool, when I get chance I will put the API in the top comment. |
This PR adds an indexedDB wrapper to
gloo_storage
. Included is some docs explaining design decisions. It would be really nice if we could provide type safety for transactions (commit on drop of some transaction object) but this isn't possible because in JS transactions auto-commit the first time a task on the task queue ends without an active idb operation ongoing (so-called transaction auto-commit).Indexed DB wrapper
This section explains how the IndexedDB wrapper works, and explains the design decisions taken, with
alternatives and rationale for the chosen option.
Intro
IndexedDB
(a.k.aIDB
) is an object-store type database defined by the World-wide Web Consortium(specification). It replaces earlier
WebSQL
, and is preferred over the Web Storage APIwhen working with larger amounts of data, as it will not block the JavaScript thread when fetching or storing
data. Features include
can be inspected and then modified/deleted without interrupting the cursor
Guide
IndexedDB is a database that can be used in web browsers or other places JavaScript is used. When you change
or get data from the database, the operation doesn't finish instantly. Instead, you get an request back, and a
way to be notified when it has finished. The IDB wrapper in
gloo-storage
turns these requests intoFuture
sso you can use them the same way you'd use any other Rust future.
TODO more content. I don't think I need to recreate the docs, a worked example would be more useful and shorter.
Motivation
The reason for writing a wrapper for IDB is that it is really quite hard to use, even if we were writing
JavaScript. It requires the user to understand the purpose and operation of [IDBRequest], which looks
quite like a Rust future, but where concepts are named differently and callbacks are required to respond to
events. We can wrap the API to provide much more ideomatic Rust APIs with no or very little overhead over
what would be required anyway. We can also close off entire classes of errors using the type system, which
in JavaScript require exceptions (JS is loosely typed so cannot do what we can).
Internal explanation
The internal workings of our wrapper.
[IDBRequest]
The
[IDBRequest]
interface is the core of the IDB API. It provides the mechanism for operations to take placeasynchronously. It is created immediately by many IDB operations, and will fire events when the operation
completes, whether successfully or otherwise. Usually it completes at most once, but when using cursors it can
complete many times, moving back and forth between pending and done. The main methods on the interface are:
readyState
: contains the state of the operation, and is a string matching either"pending"
or"done"
.Its value won't change within the same JS task, but can change between tasks.
result
: contains the result of the request (or this result if it's a sequence) if it was successful.error
: contains the error if the request failed, orNoError
if it succeeded.transaction
: the transaction object that created this request. Not all requests are associatedwith transactions.
success
event: indicates the request has succeeded and itsresult
is available.error
event: indicates the request has failed and theerror
is available.We will ignore the
source
property, because the user will always already have access to the source (sincethey needed it to create the request), and we can in theory prevent some errors by preventing it from being
accessed here.
The API above looks almost identical to a Rust
Future
. When we poll a request, we check itsreadyState
tosee if we can complete straight away. If not, we make sure we are woken on completion by setting event handlers
for success and error. Putting this all together we get the implementation:
I'm not going to post all of the implementation, but since this is the core of the whole wrapper, it's worth
reproducing verbatim. To put it simply, the operation of both the
Future
interface in Rust and the callbackinterface of
IDBRequest
are the same: check if the operation is already done, then if not ask to be toldwhen it is. The only difference is the use of callbacks vs. task wakers, which this wrapper handles
transparently, so the user doesn't even need to be aware.
There are two other variants of the
Request
data. One isOpenDbRequest
, which works the same except forhandling an extra event
blocked
. This event tells the user if the database is already in use. Since thisoften indicates a programmer mistake, we provide an option to turn this event into an error.
The other is a wrapper for
IDBRequest
s that can change back fromdone
topending
. This only happenswhen using cursors, so we create two different wrappers to make the other cases simpler. The streaming
version implements
Stream
, which is Rust's abstraction for futures that return multiple times.Transactions, Object stores, Indexes, and Cursors
A lot of the contents of the IDB wrapper just forward straight to their
web_sys
analogs, so there reallyisn't that much to say about them. One exception to this rule is how the different types of transactions
are handled. These are
"readonly"
,"readwrite"
, and"versionchange"
. The three types of transactiondetermine what you can do with the database: you need to be in a versionchange transaction to alter the
structure of the database (the other two types are self-explanatory). In the IDB spec this information is
stored internally, but we have the opportunity to use Rust's type system to make this explicit. The advantage
of this approach is that we can catch invalid use of the API (for example updating a record in a readonly
transaction) at compile-time! This is currently implemented using "uninhabited enums": enums that have no
variants so can never be constructed. They are simply used as generic markers so that our other structs
know what operations they are allowed to do.
Options
Where there is more than one argument to a function, this wrapper prefer using a special "Options" struct
(for example
ObjectStoreOptions
), with aDefault
impl so you can do e.g.ObjectStoreOptions::default()
if you don't want to change any of the defaults. They use the non-borrowing builder struct pattern.
Key and KeyPath
The
Key
andKeyPath
are new types introduced in this wrapper: the equivalent is untyped in JS. Stronglytyping this values enables us to catch more errors when they are created rather than used, which should aid
debugging. We can also provide more useful error messages.
TODO note: currently the
KeyPath
uses the custom trait alternative design. I'm planning to change thisbefore marking the work ready for merge.
Query
The concept of a "query" doesn't exist in IDB, but this wrapper introduces it to encapsulate filtering a
set of records. It can be 'all records', a single record, or a range of records. The JS equivalent involves
calling different methods depending on the type of query, and if its a range, the type of range as well.
Opening a database
When opening a database, we have to provide the user with some way of specifying how the database should
be updated. This wrapper uses a callback, with the new and old versions provided, along with an object for
modifying the database.
Error handling
I think there is a potentially really good design for error handling, where most error types are the same,
but I haven't quite got it nailed yet. Will update once it's sorted.