1. Introduction
This section is non-normative.
The
localStorage
API
is
widely
used,
and
loved
for
its
simplicity.
However,
its
synchronous
nature
leads
to
terrible
performance
and
cross-window
synchronization
issues.
This specification proposes a new API, called KV storage, which is intended to provide an analogously simple interface, while being asynchronous. Along the way, it embraces some additional goals:
-
Layer on top of Indexed Database. This avoids introducing a new type of storage for user agents and web developers to manage, and allows an upgrade path to full IndexedDB usage if a web developer outgrows the KV storage interface. [INDEXEDDB-2]
-
Modernize the API surface. Modern key/value stores in the platform, such as the
Cache
orHeaders
APIs, have aligned around the operation names given by JavaScript’sMap
. We follow their example. As a bonus, this allows us to avoid the legacy named properties feature that theStorage
interface uses. -
Support isolated storage areas.
localStorage
requires careful namespacing of keys to use robustly in a multi-actor environment. Popular libraries meant to replace it, like localForage , have included a way to create new storage areas beyond the default one. -
Be a built-in module. There are a variety of reason to support expanding the web’s API surface via built-in modules, instead of built-in globals. We want KV storage to be one of the earliest of these, and are in active collaboration with the authors of the Standard Library Proposal to be grounded in their work. [JSSTDLIB]
localStorage
example
to
use
KV
storage
might
look
like
the
following:
< p > You have viewed this page< span id = "count" > an untold number of</ span > time(s).</ p > < script type = "module" > import { storage} from"std:kv-storage" ; ( async() => { let pageLoadCount= await storage. get( "pageLoadCount" ) || 0 ; ++ pageLoadCount; document. querySelector( "#count" ). textContent= pageLoadCount; await storage. set( "pageLoadCount" , pageLoadCount); })(); </ script >
As a side note, observe how, in contrast to the original example which performs up to five storage operations, our example only performs two. Also, it updates the UI as soon as possible, instead of delaying the UI update until we’ve set the new page load count.
The
KV
storage
API
design
can
take
some
credit
for
this,
as
by
forcing
us
to
explicitly
state
our
await
points,
it
makes
it
more
obvious
that
we’re
performing
a
potentially-expensive
storage
operation.
2.
The
std:kv-storage
built-in
module
-
import * as kvs from "std:kv-storage"
-
Imports the KV storage API’s namespace object as the variable kvs .
If the module is not imported in a secure context , the
import
statement will cause a "SecurityError
"DOMException
, as persistent storage is a powerful feature. -
kvs . storage
-
Returns the default storage area. It is a pre-constructed instance of the
StorageArea
class, meant to be a convenience similar tolocalStorage
. -
kvs . StorageArea
-
Returns the constructor for the
StorageArea
class, to allow the creation of isolated storage areas.
This
specification
defines
a
new
built-in
module.
Tentatively,
depending
on
further
discussions,
we
use
the
specifier
"
std:kv-storage
"
to
denote
it
for
now.
This
is
not
final
and
is
subject
to
change
depending
on
the
details
of
how
built-in
modules
end
up
working.
[JSSTDLIB]
Its exports are the following:
-
storage
-
An instance of the
StorageArea
class, created as if by Construct (StorageArea
, « "default
" »). -
StorageArea
-
The
StorageArea
class
import { storage, StorageArea} from"std:kv-storage" ;
In addition to establishing its exports, evaluating the module must perform the following steps:
-
If the current settings object is not contextually secure , throw a "
SecurityError
"DOMException
.
3.
The
StorageArea
class
Upon
evaluating
the
std:kv-storage
module,
the
StorageArea
class
must
be
created
in
the
current
realm
.
The
result
must
be
equivalent
to
evaluating
the
following
JavaScript
code,
with
the
following
two
exceptions:
-
The constructor, method, and getter bodies must behave as specified below instead of being the no-ops shown in this code block.
-
HostHasSourceTextAvailable must return false for all function objects (i.e. the constructor, methods, and getter) created.
class StorageArea{ constructor( name) { /* see below */ } set( key, value) { /* see below */ } get( key) { /* see below */ } delete ( key) { /* see below */ } clear() { /* see below */ } keys() { /* see below */ } values() { /* see below */ } entries() { /* see below */ } get backingStore() { /* see below */ } }
The
prototype
property
of
StorageArea
must
additionally
have
a
@@asyncIterator
property,
whose
value
is
equal
to
the
same
function
object
as
the
original
value
of
StorageArea.prototype.entries()
.
The
intention
of
defining
the
StorageArea
class
in
this
way,
using
a
skeleton
JavaScript
class
definition,
is
to
automatically
establish
the
various
properties
of
the
class,
its
methods,
and
its
getter,
which
otherwise
need
to
be
specified
in
tedious
detail.
For
example,
this
automatically
establishes
the
length
and
name
properties
of
all
these
functions,
their
property
descriptors,
their
prototype
and
constructor
properties,
etc.
And
it
does
so
in
a
way
that
is
consistent
with
what
a
JavaScript
developer
would
expect.
Why not use Web IDL?
Apart from the above novel technique, there are two commonly-seen alternatives for defining JavaScript classes. The JavaScript specification, as well as the Streams Standard, defer to the " ECMAScript Standard Built-in Objects " section of the JavaScript specification, which defines many defaults. The more popular alternative, however, is to use Web IDL. Why aren’t we using that?
Web IDL has a few minor mismatches with our goals for built-in modules:
-
Its automatically-generated brand checks are both unforgeable and cross-realm, which is not accomplishable in JavaScript. Our brand checks are same-realm-only, as we would like built-in modules to not have special privileges in this regard over non-built-in ones.
-
It does not have a mechanism for exposing classes inside modules; instead they are always exposed on some set of global objects.
-
It produces methods and accessors that are enumerable, which does not match the natural JavaScript implementation. This would make it more difficult to implement a Web IDL-specified built-in module in JavaScript. (But not impossible.)
-
The generic nature of Web IDL means that it is best implemented using code generation. However, most implementers currently do not have a Web IDL bindings generator that wraps JavaScript; using Web IDL would virtually require them to either implement the built-in modules in C++, or create such a bindings generator. Furthermore, the wrappers end up being quite large; see an example .
None of these mismatches are fatal. We could switch this specification to Web IDL, with appropriate extensions for solving the first two problems, if that ends up being desired. We recognize that the goals for built-in modules are still under active discussion, and the above might not end up being important in the end. But for now, we’re experimenting with this different—and more aligned-with-JavaScript-modules—mechanism of specifying a class definition.
Each
StorageArea
instance
must
also
contain
the
[[DatabaseName]]
,
[[DatabasePromise]]
,
and
[[BackingStoreObject]]
internal
slots.
The
following
is
a
non-normative
summary
of
their
meaning:
- [[DatabaseName]]
- A string containing the name of the backing IndexedDB database.
- [[DatabasePromise]]
-
A
promise
for
an
IDBDatabase
object, lazily initialized when performing any database operation . - [[BackingStoreObject]]
-
The
object
returned
by
the
backingStore
getter, cached to ensure identity across gets.
A
JavaScript
value
val
brand
checks
as
a
StorageArea
if
Type
(
val
)
is
Object,
val
has
a
[[DatabaseName]]
internal
slot,
and
val
’s
relevant
realm
is
equal
to
the
current
realm
.
The
realm
check
here
gives
us
semantics
identical
to
using
JavaScript’s
WeakMap
or
the
proposed
private
class
fields.
This
ensures
StorageArea
does
not
use
any
magic,
like
the
platform’s
usual
cross-realm
brand
checks,
which
go
beyond
what
can
be
implemented
in
JavaScript.
[ECMA-262]
[CLASS-FIELDS]
3.1. constructor( name )
-
storage = new
StorageArea
( name ) -
Creates a new
StorageArea
that provides an async key/value store view onto an IndexedDB database named`kv-storage:${name}`
.This does not actually open or create the database yet; that is done lazily when other methods are called. This means that all other methods can reject with database-related exceptions in failure cases.
-
Let area be this
StorageArea
object. -
Let nameString be ToString ( name ).
-
Set area . [[DatabaseName]] to the concatenation of "
kv-storage:
" and nameString . -
Set area . [[DatabasePromise]] to null.
-
Set area . [[BackingStoreObject]] to null.
3.2. set( key , value )
-
await storage .
set
( key , value ) -
Asynchronously stores the given value so that it can later be retrieved by the given key .
Keys have to follow the same restrictions as IndexedDB keys: roughly, a key can be a number, string, array,
Date
,ArrayBuffer
,DataView
, typed array , or an array of these. Invalid keys will cause the returned promise to reject with a "DataError
"DOMException
.Values can be any value that can be structured-serialized for storage . Un-serializable values will cause a "
DataCloneError
"DOMException
. The value undefined will cause the corresponding entry to be deleted.The returned promise will fulfill with undefined on success.
-
If key is not allowed as a key , return a promise rejected with a "
DataError
"DOMException
. -
Return the result of performing a database operation given this object, "
readwrite
", and the following steps operating on transaction and store :-
If value is undefined, then
-
Perform the steps listed in the description of
IDBObjectStore
'sdelete()
method on store , given the argument key .
-
-
Otherwise,
-
Perform the steps listed in the description of
IDBObjectStore
'sput()
method on store , given the arguments value and key .
-
-
Let promise be a new promise .
-
Add a simple event listener to transaction for "
complete
" that resolves promise with undefined. -
Add a simple event listener to transaction for "
error
" that rejects promise with transaction ’s error . -
Add a simple event listener to transaction for "
abort
" that rejects promise with transaction ’s error . -
Return promise .
-
3.3. get( key )
-
value = await storage .
get
( key ) -
Asynchronously retrieves the value stored at the given key , or undefined if there is no value stored at key .
Values retrieved will be structured-deserialized from their original form.
-
If key is not allowed as a key , return a promise rejected with a "
DataError
"DOMException
. -
Return the result of performing a database operation given this object, "
readonly
", and the following steps operating on transaction and store :-
Let request be the result of performing the steps listed in the description of
IDBObjectStore
'sget()
method on store , given the argument key . -
Let promise be a new promise .
-
Add a simple event listener to request for "
success
" that resolves promise with request ’s result . -
Add a simple event listener to request for "
error
" that rejects promise with request ’s error . -
Return promise .
-
3.4. delete( key )
-
await storage .
delete
( key ) -
Asynchronously deletes the entry at the given key . This is equivalent to storage .
set
( key , undefined).The returned promise will fulfill with undefined on success.
-
If key is not allowed as a key , return a promise rejected with a "
DataError
"DOMException
. -
Return the result of performing a database operation given this object, "
readwrite
", and the following steps operating on transaction and store :-
Perform the steps listed in the description of
IDBObjectStore
'sdelete()
method on store , given the argument key . -
Let promise be a new promise .
-
Add a simple event listener to transaction for "
complete
" that resolves promise with undefined. -
Add a simple event listener to transaction for "
error
" that rejects promise with transaction ’s error . -
Add a simple event listener to transaction for "
abort
" that rejects promise with transaction ’s error . -
Return promise .
-
3.5. clear()
-
await storage .
clear
() -
Asynchronously deletes all entries in this storage area.
This is done by actually deleting the underlying IndexedDB database. As such, it always can be used as a fail-safe to get a clean slate, as shown below .
The returned promise will fulfill with undefined on success.
-
Let area be this object.
-
If area does not brand check , return a promise rejected with a
TypeError
exception. -
If area . [[DatabasePromise]] is not null, return the result of transforming area . [[DatabasePromise]] by fulfillment and rejection handlers that both perform the following steps:
-
Set area . [[DatabasePromise]] to null.
-
Return the result of deleting the database given by area . [[DatabaseName]] .
-
-
Otherwise, return the result of deleting the database given by area . [[DatabaseName]] .
To delete the database given a string name :
-
Let promise be a new promise .
-
Let request be the result of performing the steps listed in the description of
IDBFactory
'sdeleteDatabase()
method on the currentIDBFactory
, given the argument name . -
If those steps threw an exception, catch the exception and reject promise with it.
-
Otherwise:
-
Add a simple event listener to request for "
success
" that resolves promise with undefined. -
Add a simple event listener to request for "
error
" that rejects promise with request ’s error .
-
-
Return promise .
// This upgrade to version 100 breaks the "cats" storage area: since StorageAreas // assume a version of 1, "cats" can no longer be used with KV storage. const openRequest= indexedDB. open( "kv-storage:cats" , 100 ); openRequest. onsuccess= () => { openRequest. onsuccess. close(); }; ( async() => { const area= new StorageArea( "cats" ); // Due to the above upgrade, all other methods will reject: try { await area. set( "fluffy" , new Cat()); } catch ( e) { // This will be reached and output a "VersionError" DOMException console. error( e); } // But clear() will delete the database entirely: await area. clear(); // Now we can use it again! await area. set( "fluffy" , new Cat()); await area. set( "tigger" , new Cat()); // Also, the version is back down to 1: console. assert( area. backingStore. version=== 1 ); })();
3.6. keys()
-
for await (const key of storage .
keys
()) { ... } -
Retrieves an async iterator containing the keys of all entries in this storage area.
Keys will be yielded in ascending order; roughly, segregated by type, and then sorted within each type. They will be key round-tripped from their original form.
The iterator provides a live view onto the storage area: modifications made to entries sorted after the last-returned one will be reflected in the iteration.
-
Let area be this object.
-
If area does not brand check , throw a
TypeError
exception. -
Return the result of creating a storage area async iterator given area and "
keys
".
await storage. set( 10 , "value 10" ); await storage. set( 20 , "value 20" ); await storage. set( 30 , "value 30" ); const keysSeen= []; for await( const keyof storage. keys()) { if ( key=== 20 ) { await storage. set( 15 , "value 15" ); await storage. delete ( 20 ); await storage. set( 25 , "value 25" ); } keysSeen. push( key); } console. log( keysSeen); // logs 10, 20, 25, 30
That
is,
calling
keys()
does
not
create
a
snapshot
as
of
the
time
it
was
called;
it
returns
a
live
asynchronous
iterator,
that
lazily
retrieves
the
next
key
after
the
last-seen
one.
3.7. values()
-
for await (const value of storage .
values
()) { ... } -
Asynchronously retrieves an array containing the values of all entries in this storage area.
Values will be ordered as corresponding to their keys; see
keys()
. They will be structured-deserialized from their original form.The iterator provides a live view onto the storage area: modifications made to entries sorted after the last-returned one will be reflected in the iteration.
-
Let area be this object.
-
If area does not brand check , throw a
TypeError
exception. -
Return the result of creating a storage area async iterator given area and "
values
".
3.8. entries()
-
for await (const [ key , value ] of storage .
entries
()) { ... }for await (const [ key , value ] of storage ) { ... }
-
Asynchronously retrieves an array of two-element
[key, value]
arrays, each of which corresponds to an entry in this storage area.Entries will be ordered as corresponding to their keys; see
keys()
. Each key and value will be key round-tripped and structured-deserialized from its original form, respectively.The iterator provides a live view onto the storage area: modifications made to entries sorted after the last-returned one will be reflected in the iteration.
-
Let area be this object.
-
If area does not brand check , throw a
TypeError
exception. -
Return the result of creating a storage area async iterator given area and "
entries
".
storage
,
you
could
use
the
following
code
to
send
all
locally-stored
entries
to
a
server:
const entries= []; for await( const entryof storage. entries()) { entries. push( entry); } fetch( "/storage-receiver" , { method: "POST" , body: entries, headers: { "Content-Type" : "application/json" } });
3.9. get backingStore()
-
{ database , store , version } = storage .
backingStore
-
Asynchronously retrieves an an object containing all of the information necessary to manually interface with the IndexedDB backing store that underlies this storage area:
-
database will be a string equal to "
kv-storage:
" concatenated with the database name passed to the constructor. (For the default storage area, it will be "kv-storage:default
".) -
store will be the string "
store
". -
version will be the number 1.
It is good practice to use the
backingStore
property to retrieve this information, instead of memorizing the above factoids. -
-
Let area be this object.
-
If area does not brand check , throw a
TypeError
exception. -
If area . [[BackingStoreObject]] is null, then:
-
Let backingStoreObject be ObjectCreate (
%ObjectPrototype%
). -
Perform CreateDataProperty ( backingStoreObject , "
database
", area . [[DatabaseName]] ). -
Perform CreateDataProperty ( backingStoreObject , "
store
", "store
"). -
Perform CreateDataProperty ( backingStoreObject , "
version
", 1). -
Perform SetIntegrityLevel ( backingStoreObject , "
frozen
"). -
Set area . [[BackingStoreObject]] to backingStoreObject .
-
-
Return area . [[BackingStoreObject]] .
storage
like
so:
bulbasaur. onchange= () => storage. set( "bulbasaur" , bulbasaur. checked); ivysaur. onchange= () => storage. set( "ivysaur" , ivysaur. checked); venusaur. onchange= () => storage. set( "venusaur" , venusaur. checked); // ...
(Hopefully the developer quickly realizes that the above will be hard to maintain, and refactors the code into a loop. But in the meantime, their repetitive code makes for a good example, so let’s take advantage of that.)
The developer now realizes they want to add an evolution feature, e.g. for when the user transforms their Bulbasaur into an Ivysaur. They might first implement this like so:
bulbasaurEvolve. onclick= async() => { await storage. set( "bulbasaur" , false ); await storage. set( "ivysaur" , true ); };
However, our developer starts getting bug reports from their users: if the users happen to open up the checklist app in a second tab while they’re evolving in the first tab, the second tab will sometimes see that their Bulbasaur has disappeared, without ever turning into an Ivysaur! A Pokémon has gone missing!
The
solution
here
is
to
step
beyond
the
comfort
zone
of
KV
storage,
and
start
using
the
full
power
of
IndexedDB:
in
particular,
its
transactions
feature.
The
backingStore
getter
is
the
gateway
to
this
world:
const { database, store, version} = storage. backingStore; const request= indexedDB. open( database, version); request. onsuccess= () => { const db= request. result; bulbasaurEvolve. onclick= () => { const transaction= db. transaction( store, "readwrite" ); const store= transaction. objectStore( store); store. put( "bulbasaur" , false ); store. put( "ivysaur" , true ); db. close(); }; };
Satisfied with their web app’s Pokémon integrity, our developer is now happy and fulfilled. (At least, until they realize that none of their code has error handling.)
4. The storage area async iterator
Much of this section is amenable to being generalized into a reusable primitive, probably via Web IDL. See heycam/webidl#580 .
Upon
evaluating
the
std:kv-storage
module,
let
the
storage
area
async
iterator
prototype
object
be
object
obtained
via
the
following
steps
executed
in
the
current
realm
:
-
Let proto be ObjectCreate (
%IteratorPrototype%
). -
Let next be CreateBuiltinFunction (the steps of §4.2 next() ).
-
Perform CreateMethodProperty ( proto , "
next
", next ). -
Return proto .
4.1. Creation
To
create
a
storage
area
async
iterator
,
given
a
StorageArea
area
and
a
string
mode
which
is
one
of
either
"
keys
",
"
values
",
or
"
entries
":
-
Let iter be ObjectCreate ( the storage area async iterator prototype object , « [[Area]] , [[Mode]] , [[LastKey]] , [[OngoingPromise]] »).
-
Set iter . [[Area]] to area .
-
Set iter . [[Mode]] to mode .
-
Set iter . [[LastKey]] to not yet started .
-
Set iter . [[OngoingPromise]] to undefined.
-
Return iter .
The following is a non-normative summary of the internal slots that get added to objects created in such a way:
- [[Area]]
-
A
pointer
back
to
the
originating
StorageArea
, used so that the async iterator can perform database operations . - [[Mode]]
-
One
of
"
keys
", "values
", or "entries
", indicating the types of values that iteration will retrieve from the storage area. - [[LastKey]]
-
The
key
of
the
entry
that
was
most
recently
iterated
over,
used
to
perform
the
next
iteration.
Or,
if
next()
has not yet been called, it will be set to not yet started . - [[OngoingPromise]]
-
A
reference
to
the
promise
that
was
returned
by
the
most
recent
call
to
next()
, if that promise has not yet settled, or undefined if it has. Used to prevent concurrent executions of the main get the next IterResult algorithm, which would be bad because that algorithm needs to complete in order for [[LastKey]] to be set correctly.
4.2. next()
-
Let iter be this object.
-
If Type ( iter ) is not Object, or iter ’s relevant realm is not equal to the current realm , or iter does not have a [[Area]] internal slot, then return a promise rejected with a
TypeError
exception. -
Let currentOngoingPromise be iter . [[OngoingPromise]] .
-
Let resultPromise be undefined.
-
If currentOngoingPromise is not undefined, then set resultPromise to the result of transforming currentOngoingPromise by the result of getting the next IterResult given iter .
-
Otherwise, set resultPromise to the result of getting the next IterResult given iter .
-
Set iter . [[OngoingPromise]] to resultPromise .
-
Return resultPromise .
To get the next IterResult given iter :
-
Return the result of performing a database operation given iter . [[Area]] , "
read
", and the following steps operating on transaction and store :-
Let lastKey be iter . [[LastKey]] .
-
If lastKey is undefined, then return CreateIterResultObject (undefined, true).
-
Let range be the result of getting the range for lastKey .
-
Let key and iterResultValue be null.
-
Let promise be a new promise .
-
Switch on iter . [[Mode]] :
-
"
keys
" -
-
Let request be the result of performing the steps listed in the description of
IDBObjectStore
'sgetKey()
method on store , given the argument range . -
Add a simple event listener to request for "
success
" that performs the following steps: -
Add a simple event listener to request for "
error
" that rejects promise with request ’s error .
-
-
"
values
" -
-
Let keyRequest be the result of performing the steps listed in the description of
IDBObjectStore
'sgetKey()
method on store , given the argument range . -
Let valueRequest be the result of performing the steps listed in the description of
IDBObjectStore
'sget()
method on store , given the argument range . -
Add a simple event listener to valueRequest for "
success
" that performs the following steps: -
Add a simple event listener to keyRequest for "
error
" that rejects promise with keyRequest ’s error . -
Add a simple event listener to valueRequest for "
error
" that rejects promise with valueRequest ’s error .
-
-
"
entries
" -
-
Let keyRequest be the result of performing the steps listed in the description of
IDBObjectStore
'sgetKey()
method on store , given the argument range . -
Let valueRequest be the result of performing the steps listed in the description of
IDBObjectStore
'sget()
method on store , given the argument range . -
Add a simple event listener to valueRequest for "
success
" that performs the following steps:-
Set key to keyRequest ’s result .
-
Let value be valueRequest ’s result .
-
Set iterResultValue to CreateArrayFromList (« key , value »).
-
-
Add a simple event listener to keyRequest for "
error
" that rejects promise with keyRequest ’s error . -
Add a simple event listener to valueRequest for "
error
" that rejects promise with valueRequest ’s error .
-
When the above steps say to finish up , which they will do after having set key and iterResultValue appropriately, perform the following steps:
-
Set iter . [[LastKey]] to key .
-
Set iter . [[OngoingPromise]] to undefined.
-
Let done be true if key is undefined, and false otherwise.
-
Resolve promise with CreateIterResultObject ( iterResultValue , done ).
-
"
-
Return promise .
-
5. Supporting operations and concepts
To
add
a
simple
event
listener
,
given
an
EventTarget
target
,
an
event
type
string
type
,
and
a
set
of
steps
steps
:
-
Let jsCallback be a new JavaScript function object, created in the current realm , that performs the steps given by steps . Other properties of the function (such as its
name
andlength
properties, or [[Prototype]]) are unobservable, and can be chosen arbitrarily. -
Let idlCallback be the result of converting jsCallback to an
EventListener
. -
Perform the steps listed in the description of
EventTarget
'saddEventListener()
method on target given the arguments type and idlCallback .
The
current
IDBFactory
is
the
IDBFactory
instance
returned
by
the
following
steps:
-
Assert: the current global object includes
WindowOrWorkerGlobalScope
. -
Return the result of performing the steps listed in the description of the getter for
WindowOrWorkerGlobalScope
'sindexedDB
attribute on the current global object .
To
perform
a
database
operation
given
a
StorageArea
area
,
a
mode
string
mode
,
and
a
set
of
steps
steps
that
operate
on
an
IDBTransaction
transaction
and
an
IDBObjectStore
store
:
-
If area does not brand check , return a promise rejected with a
TypeError
exception. -
Assert: area . [[DatabaseName]] is a string (and in particular is not null).
-
If area . [[DatabasePromise]] is null, initialize the database promise for area .
-
Return the result of transforming area . [[DatabasePromise]] by a fulfillment handler that performs the following steps, given database :
-
Let transaction be the result of performing the steps listed in the description of
IDBDatabase
'stransaction()
method on database , given the arguments "store
" and mode . -
Let store be the result of performing the steps listed in the description of
IDBTransaction
'sobjectStore()
method on transaction , given the argument "store
". -
Return the result of performing steps , passing along transaction and store .
-
To
initialize
the
database
promise
for
a
StorageArea
area
:
-
Set area . [[DatabasePromise]] to a new promise .
-
If the current global object does not include
WindowOrWorkerGlobalScope
, reject area . [[DatabasePromise]] with aTypeError
, and return. -
Let request be the result of performing the steps listed in the description of
IDBFactory
'sopen()
method on the currentIDBFactory
, given the arguments area . [[DatabaseName]] and 1. -
If those steps threw an exception, catch the exception, reject area . [[DatabasePromise]] with it, and return.
-
Add a simple event listener to request for "
success
" that performs the following steps:-
Let database be request ’s result .
-
Check the database schema for database . If the result is false, reject area . [[DatabasePromise]] with an "
InvalidStateError
"DOMException
and abort these steps. Add a simple event listener to database for "
close
" that sets area . [[DatabasePromise]] to null.This means that if the database is closed abnormally , future invocations of perform a database operation will attempt to reopen it.
-
Add a simple event listener to database for "
versionchange
" that performs the steps listed in the description ofIDBDatabase
'sclose()
method on database , and then sets area . [[DatabasePromise]] to null.This allows attempts to upgrade the underlying database, or to delete it (e.g. via the
clear()
method), to succeed. Without this, if twoStorageArea
instances were both open referencing the same underlying database,clear()
would hang, as it only closes the connection maintained by theStorageArea
it is invoked on. -
Resolve promise with database .
-
-
Add a simple event listener to request for "
error
" that rejects promise with request ’s error . -
Add a simple event listener to request for "
upgradeneeded
" that performs the following steps:-
Let database be request ’s result .
-
Perform the steps listed in the description of
IDBDatabase
'screateObjectStore()
method on database , given the arguments "store
". -
If these steps throw an exception, catch the exception and reject area . [[DatabasePromise]] with it.
-
To
check
the
database
schema
for
an
IDBDatabase
database
:
Let objectStores be database ’s connection 's object store set .
If objectStores ’s size is not 1, return false.
Let store be objectStores [0].
If store ’s name is not "
store
", return false.If store has a key generator , return false.
If store has a key path , return false.
If any indexes reference store , return false.
Return true.
Check the database schema only needs to be called in the initial setup algorithm, initialize the database promise , since once the database connection has been opened, the schema cannot change.
A value value is allowed as a key if the following steps return true:
-
If Type ( value ) is Number or String, return true.
-
If IsArray ( value ) is true, return true.
-
If value has a [[DateValue]] internal slot, return true.
-
If value has a [[ViewedArrayBuffer]] internal slot, return true.
-
If value has an [[ArrayBufferByteLength]] internal slot, return true.
-
Return false.
Most
notably,
using
the
allowed
as
a
key
predicate
ensures
that
IDBKeyRange
objects,
or
any
other
special
object
that
is
accepted
as
a
query
in
future
IndexedDB
specification
revisions,
will
be
disallowed.
Only
straightforward
key
values
are
accepted
by
the
KV
storage
API.
Key
round-tripping
refers
to
the
way
in
which
JavaScript
values
are
processed
by
first
being
passed
through
IndexedDB’s
convert
a
value
to
a
key
operation,
then
converted
back
through
its
convert
a
key
to
a
value
operation.
Keys
returned
by
the
keys()
or
entries()
methods
will
have
gone
through
this
process.
Notably,
any
typed
arrays
or
DataView
s
will
have
been
"unwrapped",
and
returned
back
as
just
ArrayBuffer
s
containing
the
same
bytes.
Also,
similar
to
the
structured-serialization
/
deserialization
process,
any
"expando"
properties
or
other
modifications
will
not
be
preserved
by
key
round-tripping
.
For primitive string or number values, there’s no need to worry about key round-tripping ; the values are indistinguishable.
To get the range for a key key :
-
If key is not yet started , then return the result of performing the steps listed in the description of the
IDBKeyRange.lowerBound()
static method, given the argument −Infinity.The intent here is to get an unbounded key range , but this is the closest thing we can get that is representable as an
IDBKeyRange
object. It works equivalently for our purposes, but will behave incorrectly if Indexed DB ever adds keys that sort below −Infinity. See some discussion on potential future improvements . -
Otherwise, return the result of performing the steps listed listed in the description of the
IDBKeyRange.lowerBound()
static method, given the arguments lastKey and true.
The special value not yet started can be taken to be any JavaScript value that is not equal to any other program-accessible JavaScript value (but is equal to itself). It is used exclusively as an argument to the get the range for a key algorithm.
A
newly
created
object
or
symbol,
e.g.
or
,
would
satisfy
this
definition.
6. Appendix: is this API perfectly layered?
The APIs in this specification, being layered on top of Indexed DB as they are, are almost entirely well-layered, in the sense of building on low-level features in the way promoted by the Extensible Web Manifesto . (Indeed, the unusual class definition pattern was motivated by a desire to further improve this layering.) However, it fails in two ways, both around ensuring the encapsulation of the implementation: [EXTENSIBLE]
-
By requiring censorship of the output of
Function.prototype.toString()
for the functions produced. See drufball/layered-apis#7 . -
By directly invoking the algorithms of various IDL operations and attributes, instead of going through the global, potentially-overridable JavaScript APIs. (E.g., in various algorithm steps that say "performing the steps listed in the description of", or the allowed as a key algorithm which uses IsArray directly instead of going through
Array.isArray()
.) See drufball/layered-apis#6 .
Eventually we hope to introduce the ability for web authors to write code that gets these same benefits, instead of locking them up so that only web platform APIs like KV storage can achieve this level of encapsulation. That’s a separate effort, however, which is best followed in the above-linked issue threads.
Acknowledgments
The editor would like to thank Andrew Sutherland, Kenneth Rohde Christiansen, Jake Archibald, Jan Varga, Joshua Bell, and Victor Costan for their contributions to this specification.
Conformance
This specification depends on the Infra Standard. [INFRA]