diff --git a/spec.bs b/spec.bs index ad96719..d9e2af5 100644 --- a/spec.bs +++ b/spec.bs @@ -192,6 +192,25 @@ This specification proposes a new API, called KV storage, which is intended to p

The std:kv-storage built-in module

+
+[SecureContext]
+module kv-storage {
+  [Constructor(DOMString name), SameRealmBrandCheck, UnenumerableMethods]
+  interface StorageArea {
+    Promise<void> set(any key, any value);
+    Promise<any> get(any key);
+    Promise<void> delete(any key);
+    Promise<void> clear();
+
+    [=StorageArea/get the next iteration result|async_iterable<any, any>=];
+
+    [SameObject] readaonly attribute object backingStore;
+  };
+
+  readonly attribute kv-storage.StorageArea storage;
+};
+
+
import * as |kvs| from "std:kv-storage"
@@ -211,12 +230,12 @@ This specification proposes a new API, called KV storage, which is intended to p
-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]] +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" »). +:: An instance of the {{StorageArea}} class, backed by a database named "kv-storage:default". : StorageArea :: The {{StorageArea}} class @@ -228,61 +247,9 @@ Its exports are the following: -In addition to establishing its exports, evaluating the module must perform the following steps: - -1. If the [=current settings object=] is not [$Is an environment settings object contextually secure?|contextually secure$], throw a "{{SecurityError}}" {{DOMException}}. -

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: - - - -
-  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. [=StorageArea/brand check|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:
@@ -294,9 +261,7 @@ Each {{StorageArea}} instance must also contain the \[[DatabaseName]]
The object returned by the {{StorageArea/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]] +

The [SameRealmBrandCheck] [=extended attribute=] 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]]

constructor(|name|)

@@ -308,12 +273,13 @@ A JavaScript value |val| brand checks as a {{Storag

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. -

- 1. Let |area| be this {{StorageArea}} object. - 1. Let |nameString| be [$ToString$](|name|). - 1. Set |area|.[=[[DatabaseName]]=] to the concatenation of "kv-storage:" and |nameString|. - 1. Set |area|.[=[[DatabasePromise]]=] to null. - 1. Set |area|.[=[[BackingStoreObject]]=] to null. +
+The StorageArea(|name|) constructor, when +invoked, must run these steps: + + 1. Set this.[=[[DatabaseName]]=] to the concatenation of "kv-storage:" and |name|. + 1. Set this.[=[[DatabasePromise]]=] to null. + 1. Set this.[=[[BackingStoreObject]]=] to null.

set(|key|, |value|)

@@ -398,12 +364,10 @@ A JavaScript value |val| brand checks as a {{Storag
- 1. Let |area| be this object. - 1. If |area| does not [=StorageArea/brand check=], return [=a promise rejected with=] a {{TypeError}} exception. - 1. If |area|.[=[[DatabasePromise]]=] is not null, return the result of [=transforming=] |area|.[=[[DatabasePromise]]=] by fulfillment and rejection handlers that both perform the following steps: - 1. Set |area|.[=[[DatabasePromise]]=] to null. - 1. Return the result of [=deleting the database=] given by |area|.[=[[DatabaseName]]=]. - 1. Otherwise, return the result of [=deleting the database=] given by |area|.[=[[DatabaseName]]=]. + 1. If this.[=[[DatabasePromise]]=] is not null, return the result of [=transforming=] this.[=[[DatabasePromise]]=] by fulfillment and rejection handlers that both perform the following steps: + 1. Set this.[=[[DatabasePromise]]=] to null. + 1. Return the result of [=deleting the database=] given by this.[=[[DatabaseName]]=]. + 1. Otherwise, return the result of [=deleting the database=] given by this.[=[[DatabaseName]]=].
To delete the database given a string |name|: @@ -453,7 +417,34 @@ To delete the database
-

keys()

+

Iteration

+ +The {{StorageArea}} interface supports asynchronous iteration. The iteration methods are found below. + +To get the next iteration result: + +1. If this's [=relevant realm=] is not equal to the [=current realm=], then return [=a promise rejected with=] a {{TypeError}} exception. + + Issue: This should be handled by WebIDL once the various features involved land. +1. Return the result of [=performing a database operation=] given this, "readonly", and the following steps operating on |transaction| and |store|: + 1. Let |lastKey| be current state. + 1. Let |range| be the result of [=getting the range for=] |lastKey|. + 1. Let |keyRequest| be the result of performing the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/getKey()}} method on |store|, given the argument |range|. + 1. Let |valueRequest| be the result of performing the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/get()}} method on |store|, given the argument |range|. + 1. Let |promise| be [=a new promise=]. + 1. [=Add a simple event listener=] to |valueRequest| for "success" that performs the following steps: + 1. Let |key| be |keyRequest|'s [=request/result=]. + 1. If |key| is undefined, then: + 1. [=Resolve=] |promise| with undefined. + 1. Otherwise: + 1. Let |value| be |valueRequest|'s [=request/result=]. + 1. [=Resolve=] |promise| with (|key|, |value|, |key|). + 1. [=Add a simple event listener=] to |keyRequest| for "error" that [=rejects=] |promise| with |keyRequest|'s [=request/error=]. + 1. [=Add a simple event listener=] to |valueRequest| for "error" that [=rejects=] |promise| with |valueRequest|'s [=request/error=]. + 1. Return |promise|. + + +

keys()

for await (const |key| of |storage|.{{StorageArea/keys()|keys}}()) { ... } @@ -465,12 +456,6 @@ To delete the database

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.

-
- 1. Let |area| be this object. - 1. If |area| does not [=StorageArea/brand check=], throw a {{TypeError}} exception. - 1. Return the result of [=creating a storage area async iterator=] given |area| and "keys". -
-
To illustrate the live nature of the async iterator, consider the following: @@ -495,7 +480,7 @@ To delete the database That is, calling {{StorageArea/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.
-

values()

+

values()

for await (const |value| of |storage|.{{StorageArea/values()|values}}()) { ... } @@ -507,13 +492,7 @@ To delete the database

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.

-
- 1. Let |area| be this object. - 1. If |area| does not [=StorageArea/brand check=], throw a {{TypeError}} exception. - 1. Return the result of [=creating a storage area async iterator=] given |area| and "values". -
- -

entries()

+

entries()

for await (const [|key|, |value|] of |storage|.{{StorageArea/entries()|entries}}()) { ... } @@ -526,12 +505,6 @@ To delete the database

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.

-
- 1. Let |area| be this object. - 1. If |area| does not [=StorageArea/brand check=], throw a {{TypeError}} exception. - 1. Return the result of [=creating a storage area async iterator=] given |area| and "entries". -
-
Assuming you knew that that you only stored JSON-compatible types in [=storage=], you could use the following code to send all locally-stored entries to a server: @@ -566,16 +539,14 @@ To delete the database
- 1. Let |area| be this object. - 1. If |area| does not [=StorageArea/brand check=], throw a {{TypeError}} exception. - 1. If |area|.[=[[BackingStoreObject]]=] is null, then: + 1. If this.[=[[BackingStoreObject]]=] is null, then: 1. Let |backingStoreObject| be [$ObjectCreate$]({{%ObjectPrototype%}}). - 1. Perform [$CreateDataProperty$](|backingStoreObject|, "database", |area|.[=[[DatabaseName]]=]). + 1. Perform [$CreateDataProperty$](|backingStoreObject|, "database", this.[=[[DatabaseName]]=]). 1. Perform [$CreateDataProperty$](|backingStoreObject|, "store", "store"). 1. Perform [$CreateDataProperty$](|backingStoreObject|, "version", 1). 1. Perform [$SetIntegrityLevel$](|backingStoreObject|, "frozen"). - 1. Set |area|.[=[[BackingStoreObject]]=] to |backingStoreObject|. - 1. Return |area|.[=[[BackingStoreObject]]=]. + 1. Set this.[=[[BackingStoreObject]]=] to |backingStoreObject|. + 1. Return this.[=[[BackingStoreObject]]=].
@@ -624,106 +595,6 @@ To delete the database 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.)
-

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=]: - -1. Let |proto| be [$ObjectCreate$]({{%AsyncIteratorPrototype%}}). -1. Let |next| be [$CreateBuiltinFunction$](the steps of [[#storageareaasynciterator-next]]). -1. Perform [$CreateMethodProperty$](|proto|, "next", |next|). -1. Return |proto|. - -

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": - -1. Let |iter| be [$ObjectCreate$]([=the storage area async iterator prototype object=], « \[[Area]], \[[Mode]], \[[LastKey]], \[[OngoingPromise]] »). -1. Set |iter|.[=[[Area]]=] to |area|. -1. Set |iter|.[=[[Mode]]=] to |mode|. -1. Set |iter|.[=[[LastKey]]=] to [=not yet started=]. -1. Set |iter|.[=[[OngoingPromise]]=] to undefined. -1. 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 a database operation|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 {{the storage area async iterator prototype object/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 {{the storage area async iterator prototype object/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. -
- -

next()

- -1. Let |iter| be this object. -1. 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. -1. Let |currentOngoingPromise| be |iter|.[=[[OngoingPromise]]=]. -1. Let |resultPromise| be undefined. -1. If |currentOngoingPromise| is not undefined, then set |resultPromise| to the result of [=transforming=] |currentOngoingPromise| by the result of [=getting the next IterResult=] given |iter|. -1. Otherwise, set |resultPromise| to the result of [=getting the next IterResult=] given |iter|. -1. Set |iter|.[=[[OngoingPromise]]=] to |resultPromise|. -1. Return |resultPromise|. - -To get the next IterResult given |iter|: - -1. Return the result of [=performing a database operation=] given |iter|.[=[[Area]]=], "read", and the following steps operating on |transaction| and |store|: - 1. Let |lastKey| be |iter|.[=[[LastKey]]=]. - 1. If |lastKey| is undefined, then return [$CreateIterResultObject$](undefined, true). - 1. Let |range| be the result of [=getting the range for=] |lastKey|. - 1. Let |key| and |iterResultValue| be null. - 1. Let |promise| be [=a new promise=]. - 1. Switch on |iter|.[=[[Mode]]=]: -
-
"keys"
-
- 1. Let |request| be the result of performing the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/getKey()}} method on |store|, given the argument |range|. - 1. [=Add a simple event listener=] to |request| for "success" that performs the following steps: - 1. Set |key| to |request|'s [=request/result=]. - 1. Set |iterResultValue| to |key|. - 1. [=Finish up=]. - 1. [=Add a simple event listener=] to |request| for "error" that [=rejects=] |promise| with |request|'s [=request/error=]. -
-
"values"
-
- 1. Let |keyRequest| be the result of performing the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/getKey()}} method on |store|, given the argument |range|. - 1. Let |valueRequest| be the result of performing the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/get()}} method on |store|, given the argument |range|. - 1. [=Add a simple event listener=] to |valueRequest| for "success" that performs the following steps: - 1. Set |key| to |keyRequest|'s [=request/result=]. - 1. Set |iterResultValue| to |valueRequest|'s [=request/result=]. - 1. [=Finish up=]. - 1. [=Add a simple event listener=] to |keyRequest| for "error" that [=rejects=] |promise| with |keyRequest|'s [=request/error=]. - 1. [=Add a simple event listener=] to |valueRequest| for "error" that [=rejects=] |promise| with |valueRequest|'s [=request/error=]. -
-
"entries"
-
- 1. Let |keyRequest| be the result of performing the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/getKey()}} method on |store|, given the argument |range|. - 1. Let |valueRequest| be the result of performing the steps listed in the description of {{IDBObjectStore}}'s {{IDBObjectStore/get()}} method on |store|, given the argument |range|. - 1. [=Add a simple event listener=] to |valueRequest| for "success" that performs the following steps: - 1. Set |key| to |keyRequest|'s [=request/result=]. - 1. Let |value| be |valueRequest|'s [=request/result=]. - 1. Set |iterResultValue| to [$CreateArrayFromList$](« |key|, |value| »). - 1. [=Finish up=]. - 1. [=Add a simple event listener=] to |keyRequest| for "error" that [=rejects=] |promise| with |keyRequest|'s [=request/error=]. - 1. [=Add a simple event listener=] to |valueRequest| for "error" that [=rejects=] |promise| with |valueRequest|'s [=request/error=]. -
-
- - When the above steps say to finish up, which they will do after having set |key| and |iterResultValue| appropriately, perform the following steps: - 1. Set |iter|.[=[[LastKey]]=] to |key|. - 1. Set |iter|.[=[[OngoingPromise]]=] to undefined. - 1. Let |done| be true if |key| is undefined, and false otherwise. - 1. [=Resolve=] |promise| with [$CreateIterResultObject$](|iterResultValue|, |done|). - 1. Return |promise|. -

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|: @@ -744,7 +615,6 @@ The current IDBFactory is the {{IDBFactory}} instance re 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|:
- 1. If |area| does not [=StorageArea/brand check=], return [=a promise rejected with=] a {{TypeError}} exception. 1. Assert: |area|.[=[[DatabaseName]]=] is a string (and in particular is not null). 1. If |area|.[=[[DatabasePromise]]=] is null, [=initialize the database promise=] for |area|. 1. Return the result of [=transforming=] |area|.[=[[DatabasePromise]]=] by a fulfillment handler that performs the following steps, given |database|: @@ -825,9 +695,9 @@ The special value not yet started can be taken to be any JavaScript v

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]] +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. 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. +* The output of Function.prototype.toString() for the functions produced is censored. 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.