diff --git a/Cargo.lock b/Cargo.lock index c0c98e1fcd073b..aa153b498ae620 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1178,8 +1178,10 @@ dependencies = [ name = "deno_webstorage" version = "0.37.0" dependencies = [ + "async-trait", "deno_core", "deno_web", + "fallible-iterator", "rusqlite", "serde", ] @@ -3345,6 +3347,7 @@ dependencies = [ "hashlink", "libsqlite3-sys", "memchr", + "serde_json", "smallvec", ] diff --git a/core/bindings.rs b/core/bindings.rs index 91087799a6dc8a..53d53059972fc5 100644 --- a/core/bindings.rs +++ b/core/bindings.rs @@ -911,6 +911,7 @@ fn decode( struct SerializeDeserialize<'a> { host_objects: Option>, + disallow_sab: bool, } impl<'a> v8::ValueSerializerImpl for SerializeDeserialize<'a> { @@ -929,6 +930,9 @@ impl<'a> v8::ValueSerializerImpl for SerializeDeserialize<'a> { scope: &mut HandleScope<'s>, shared_array_buffer: Local<'s, SharedArrayBuffer>, ) -> Option { + if self.disallow_sab { + return None; + } let state_rc = JsRuntime::state(scope); let state = state_rc.borrow_mut(); if let Some(shared_array_buffer_store) = &state.shared_array_buffer_store { @@ -1055,6 +1059,7 @@ fn serialize( let options = options.unwrap_or(SerializeDeserializeOptions { host_objects: None, transfered_array_buffers: None, + disallow_sab: false, }); let host_objects = match options.host_objects { @@ -1079,7 +1084,10 @@ fn serialize( None => None, }; - let serialize_deserialize = Box::new(SerializeDeserialize { host_objects }); + let serialize_deserialize = Box::new(SerializeDeserialize { + host_objects, + disallow_sab: options.disallow_sab, + }); let mut value_serializer = v8::ValueSerializer::new(scope, serialize_deserialize); @@ -1150,6 +1158,8 @@ fn serialize( struct SerializeDeserializeOptions<'a> { host_objects: Option>, transfered_array_buffers: Option>, + #[serde(default)] + disallow_sab: bool, } fn deserialize( @@ -1177,6 +1187,7 @@ fn deserialize( let options = options.unwrap_or(SerializeDeserializeOptions { host_objects: None, transfered_array_buffers: None, + disallow_sab: false, }); let host_objects = match options.host_objects { @@ -1201,7 +1212,10 @@ fn deserialize( None => None, }; - let serialize_deserialize = Box::new(SerializeDeserialize { host_objects }); + let serialize_deserialize = Box::new(SerializeDeserialize { + host_objects, + disallow_sab: options.disallow_sab, + }); let mut value_deserializer = v8::ValueDeserializer::new(scope, serialize_deserialize, &zero_copy); diff --git a/ext/web/02_event.js b/ext/web/02_event.js index 122add2114ee70..e36b33ad9d277f 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -1333,6 +1333,7 @@ listenerCount, }; window.__bootstrap.event = { + _canceledFlag, setIsTrusted, setTarget, defineEventHandler, diff --git a/ext/webidl/00_webidl.js b/ext/webidl/00_webidl.js index a7f0597b10e2bd..04629a1019b084 100644 --- a/ext/webidl/00_webidl.js +++ b/ext/webidl/00_webidl.js @@ -617,6 +617,16 @@ converters.DOMString, ); + converters["sequence or DOMString"] = (V, opts) => { + // Union for (sequence or DOMString) + if (type(V) === "Object" && V !== null) { + if (V[SymbolIterator] !== undefined) { + return converters["sequence"](V, opts); + } + } + return converters.DOMString(V, opts); + }; + function requiredArguments(length, required, opts = {}) { if (length < required) { const errMsg = `${ diff --git a/ext/websocket/01_websocket.js b/ext/websocket/01_websocket.js index 2c6337eef9a280..8805520defff91 100644 --- a/ext/websocket/01_websocket.js +++ b/ext/websocket/01_websocket.js @@ -31,16 +31,6 @@ SymbolIterator, } = window.__bootstrap.primordials; - webidl.converters["sequence or DOMString"] = (V, opts) => { - // Union for (sequence or DOMString) - if (webidl.type(V) === "Object" && V !== null) { - if (V[SymbolIterator] !== undefined) { - return webidl.converters["sequence"](V, opts); - } - } - return webidl.converters.DOMString(V, opts); - }; - webidl.converters["WebSocketSend"] = (V, opts) => { // Union for (Blob or ArrayBufferView or ArrayBuffer or USVString) if (ObjectPrototypeIsPrototypeOf(BlobPrototype, V)) { diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js new file mode 100644 index 00000000000000..64b8a6f964dadc --- /dev/null +++ b/ext/webstorage/02_indexeddb.js @@ -0,0 +1,2892 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +/// + +((window) => { + const core = window.Deno.core; + const webidl = window.__bootstrap.webidl; + const { DOMException } = window.__bootstrap.domException; + const { defineEventHandler, _canceledFlag } = window.__bootstrap.event; + const { assert } = window.__bootstrap.infra; + const { Deferred } = window.__bootstrap.streams; + const { + NumberIsNaN, + ArrayIsArray, + Date, + SafeArrayIterator, + ObjectPrototypeHasOwnProperty, + DatePrototypeGetMilliseconds, + MapPrototypeGet, + MapPrototypeDelete, + ArrayPrototypeSort, + Set, + SetPrototypeHas, + SetPrototypeAdd, + MathMin, + MathFloor, + MapPrototypeKeys, + } = window.__bootstrap.primordials; + + webidl.converters.IDBTransactionMode = webidl.createEnumConverter( + "IDBTransactionMode", + [ + "readonly", + "readwrite", + "versionchange", + ], + ); + + webidl.converters.IDBTransactionDurability = webidl.createEnumConverter( + "IDBTransactionDurability", + [ + "default", + "strict", + "relaxed", + ], + ); + + webidl.converters.IDBTransactionOptions = webidl.createDictionaryConverter( + "IDBTransactionOptions", + [ + { + key: "durability", + converter: webidl.converters.IDBTransactionDurability, + defaultValue: "default", + }, + ], + ); + + webidl.converters.IDBObjectStoreParameters = webidl.createDictionaryConverter( + "IDBObjectStoreParameters", + [ + { + key: "keyPath", + converter: webidl.createNullableConverter( + webidl.converters["sequence or DOMString"], + ), + defaultValue: null, + }, + { + key: "autoIncrement", + converter: webidl.converters.boolean, + defaultValue: false, + }, + ], + ); + + webidl.converters.IDBCursorDirection = webidl.createEnumConverter( + "IDBCursorDirection", + [ + "next", + "nextunique", + "prev", + "prevunique", + ], + ); + + webidl.converters.IDBIndexParameters = webidl.createDictionaryConverter( + "IDBIndexParameters", + [ + { + key: "unique", + converter: webidl.converters.boolean, + defaultValue: false, + }, + { + key: "multiEntry", + converter: webidl.converters.boolean, + defaultValue: false, + }, + ], + ); + + /** + * @param input {any} + * @param seen {Set} + * @returns {(Key | null)} + */ + // Ref: https://w3c.github.io/IndexedDB/#convert-a-value-to-a-key + function valueToKey(input, seen = new Set()) { + if (SetPrototypeHas(seen, input)) { + return null; + } + if (webidl.type(input) === "Number") { + if (NumberIsNaN(input)) { + return null; + } else { + return { + type: "number", + value: input, + }; + } + } else if (input instanceof Date) { + const ms = DatePrototypeGetMilliseconds(input); + if (NumberIsNaN(ms)) { + return null; + } else { + return { + type: "date", + value: input, + }; + } + } else if (webidl.type(input) === "String") { + return { + type: "string", + value: input, + }; + } else if (ArrayIsArray(input)) { + SetPrototypeAdd(seen, input); + const keys = []; + for (const entry of input) { + const key = valueToKey(entry, seen); + if (key === null) { + return null; + } + keys.push(key); + } + return { + type: "array", + value: keys, + }; + } else { + try { + const value = webidl.converters.BufferSource(input); + return { + type: "binary", + value: value.slice(), + }; + } catch (_) { + return null; + } + } + } + + // Ref: https://w3c.github.io/IndexedDB/#convert-a-value-to-a-multientry-key + function valueToMultiEntryKey(input) { + if (ArrayIsArray(input)) { + const seen = new Set([input]); + const keys = []; + for (const entry of input) { + const key = valueToKey(entry, seen); + if ( + key !== null && + keys.find((item) => compareTwoKeys(item, key)) === undefined + ) { + keys.push(key); + } + } + return { + type: "array", + value: keys, + }; + } else { + return valueToKey(input); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#convert-a-value-to-a-key-range + function valueToKeyRange(value, nullDisallowed) { + if (value instanceof IDBKeyRange) { + return value; + } + if (value === undefined || value === null) { + if (nullDisallowed) { + throw new DOMException("", "DataError"); + } else { + return createRange(null, null); + } + } + const key = valueToKey(value); + if (key === null) { + throw new DOMException("", "DataError"); + } + return createRange(key, key); + } + + // Ref: https://w3c.github.io/IndexedDB/#compare-two-keys + function compareTwoKeys(a, b) { + const { type: ta, value: va } = a; + const { type: tb, value: vb } = b; + + if (ta !== tb) { + if (ta === "array") { + return 1; + } else if (tb === "array") { + return -1; + } else if (ta === "binary") { + return 1; + } else if (tb === "binary") { + return -1; + } else if (ta === "string") { + return 1; + } else if (tb === "string") { + return -1; + } else if (ta === "number") { + return 1; + } else if (tb === "number") { + return -1; + } else if (ta === "date") { + return 1; + } else { + assert(tb === "date"); + return -1; + } + } + + switch (ta) { + case "number": + case "date": { + if (va > vb) { + return 1; + } else if (va < vb) { + return -1; + } else { + return 0; + } + } + case "string": { + if (va < vb) { + return -1; + } else if (vb < va) { + return 1; + } else { + return 0; + } + } + case "binary": { + if (va < vb) { + return -1; + } else if (vb < va) { + return -1; + } else { + return 0; + } + } + case "array": { + const len = MathMin(va.length, vb.length); + for (let i = 0; i < len; i++) { + const c = compareTwoKeys(va[i], vb[i]); + if (c !== 0) { + return c; + } + } + if (va.length > vb.length) { + return 1; + } else if (va.length < vb.length) { + return -1; + } else { + return 0; + } + } + } + } + + // Ref: https://w3c.github.io/IndexedDB/#convert-a-key-to-a-value + function keyToValue(key) { + switch (key.type) { + case "number": + return Number(key.value); + case "string": + return String(key.value); + case "date": + return new Date(key.value); + case "binary": + return new Uint8Array(key.value).buffer; + case "array": { + return key.value.map(keyToValue); + } + } + } + + // Ref: https://w3c.github.io/IndexedDB/#valid-key-path + function isValidKeyPath(key) { + if (typeof key === "string") { + if (key.length === 0) { + return true; + } else { + } + // TODO: figure out IdentifierName (https://tc39.es/ecma262/#prod-IdentifierName) + } else if (ArrayIsArray(key)) { + return key.every(isValidKeyPath); + } else { + return false; + } + } + + // Ref: https://w3c.github.io/IndexedDB/#check-that-a-key-could-be-injected-into-a-value + function checkKeyCanBeInjectedIntoValue(value, keyPath) { + const identifiers = keyPath.split("."); + assert(identifiers.length !== 0); + identifiers.pop(); + for (const identifier of identifiers) { + if (webidl.type(value) !== "Object") { + return false; + } + if (!ObjectPrototypeHasOwnProperty(value, identifier)) { + return true; + } + value = value[identifier]; + } + return webidl.type(value) === "Object"; + } + + // Ref: https://w3c.github.io/IndexedDB/#inject-a-key-into-a-value-using-a-key-path + function injectKeyIntoValueUsingKeyPath(value, key, keyPath) { + const identifiers = keyPath.split("."); + assert(identifiers.length !== 0); + const last = identifiers.pop(); + for (const identifier of identifiers) { + assert(webidl.type(value) === "Object"); + if (!ObjectPrototypeHasOwnProperty(value, identifier)) { + value[identifier] = {}; + } + value = value[identifier]; + } + assert(webidl.type(value) === "Object"); + value[last] = keyToValue(key); + } + + // Ref: https://w3c.github.io/IndexedDB/#clone + function clone(transaction, value) { + assert(transaction[_state] === "active"); + transaction[_state] = "inactive"; + const serialized = core.serialize(value, { + disallowSab: true, + }); + const cloned = core.deserialize(serialized); + transaction[_state] = "active"; + return cloned; + } + + // Ref: https://w3c.github.io/IndexedDB/#abort-a-transaction + /** + * @param transaction {IDBTransaction} + * @param error {any} + */ + function abortTransaction(transaction, error) { + core.opSync("op_indexeddb_transaction_abort", transaction[_rid]); + if (transaction[_mode] === "versionchange") { + abortUpgradeTransaction(transaction); + } + transaction[_state] = "finished"; + if (error !== null) { + transaction[_error] = error; + } + for (const request of transaction[_requestList]) { + // TODO: abort the steps to asynchronously execute a request + request[_processedDeferred].resolve(); + request[_done] = true; + request[_result] = undefined; + request[_error] = new DOMException("", "AbortError"); + request.dispatchEvent( + new Event("error", { + bubbles: true, + cancelable: true, + }), + ); + } + if (transaction[_mode] === "versionchange") { + transaction[_connection][_upgradeTransaction] = null; + } + transaction.dispatchEvent( + new Event("abort", { + bubbles: true, + }), + ); + if (transaction[_mode] === "versionchange") { + // TODO: 6.3.: the transaction should have an openrequest + // TODO: add a global map for openrequests and find by transaction + } + } + + // Ref: https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction + function abortUpgradeTransaction(transaction) { + // TODO + } + + const _failure = Symbol("failure"); + // Ref: https://w3c.github.io/IndexedDB/#extract-a-key-from-a-value-using-a-key-path + function extractKeyFromValueUsingKeyPath(value, keyPath, multiEntry) { + const r = evaluateKeyPathOnValue(value, keyPath); + if (r === _failure) { + return _failure; + } + return valueToKey(!multiEntry ? r : valueToMultiEntryKey(r)); + } + + // Ref: https://w3c.github.io/IndexedDB/#evaluate-a-key-path-on-a-value + function evaluateKeyPathOnValue(value, keyPath) { + if (ArrayIsArray(keyPath)) { + const result = []; + for (let i = 0; i < keyPath.length; i++) { + const key = evaluateKeyPathOnValue(value, keyPath[i]); // spec is wrong, arguments are reversed. + if (key === _failure) { + return _failure; + } + result[i] = key; + } + return result; + } + if (keyPath === "") { + return value; + } + const identifiers = keyPath.split("."); + for (const identifier of identifiers) { + if (webidl.type(value) === "String" && identifier === "length") { + value = value.length; + } else if (ArrayIsArray(value) && identifier === "length") { + value = value.length; + } else if (value instanceof Blob && identifier === "size") { + value = value.size; + } else if (value instanceof Blob && identifier === "type") { + value = value.type; + } else if (value instanceof File && identifier === "name") { + value = value.name; + } else if (value instanceof File && identifier === "lastModified") { + value = value.lastModified; + } else { + if (type(value) !== "Object") { + return _failure; + } + if (!ObjectPrototypeHasOwnProperty(value, identifier)) { + return _failure; + } + value = value[identifier]; + if (value === undefined) { + return _failure; + } + } + } + return value; + } + + // Ref: https://w3c.github.io/IndexedDB/#asynchronously-execute-a-request + function asynchronouslyExecuteRequest(source, operation, request) { + assert(source[_transaction][_state] === "active"); + if (!request) { + request = new IDBRequest(); + request[_source] = source; + } + source[_transaction][_requestList].push(request); + + // TODO: use .then + (async () => { + // TODO: 5.1 + let errored = false; + let result; + try { + result = await operation(); + } catch (e) { + if (source[_transaction][_state] === "committing") { + abortTransaction(source[_transaction], e); + return; + } else { + result = e; + // TODO: revert changes made by operation + errored = true; + } + } + request[_processedDeferred].resolve(); + source[_transaction][_requestList].slice( + source[_transaction][_requestList].findIndex((r) => r === request), + 1, + ); + request[_done] = true; + if (errored) { + request[_result] = undefined; + request[_error] = result; + + // Ref: https://w3c.github.io/IndexedDB/#fire-an-error-event + // TODO(@crowlKats): support legacyOutputDidListenersThrowFlag + const event = new Event("error", { + bubbles: true, + cancelable: true, + }); + if (request[_transaction][_state] === "inactive") { + request[_transaction][_state] = "active"; + } + request.dispatchEvent(event); + if (request[_transaction][_state] === "active") { + request[_transaction][_state] = "inactive"; + if (!event[_canceledFlag]) { + abortTransaction(request[_transaction], request[_error]); + return; + } + if (request[_transaction][_requestList].length === 0) { + commitTransaction(request[_transaction]); + } + } + } else { + request[_result] = result; + request[_error] = undefined; + + // Ref: https://w3c.github.io/IndexedDB/#fire-a-success-event + // TODO(@crowlKats): support legacyOutputDidListenersThrowFlag + const event = new Event("success", { + bubbles: false, + cancelable: false, + }); + if (request[_transaction][_state] === "inactive") { + request[_transaction][_state] = "active"; + } + request.dispatchEvent(event); + if (request[_transaction][_state] === "active") { + request[_transaction][_state] = "inactive"; + if (request[_transaction][_requestList].length === 0) { + commitTransaction(request[_transaction]); + } + } + } + })(); + return request; + } + + // Ref: https://w3c.github.io/IndexedDB/#commit-a-transaction + function commitTransaction(transaction) { + transaction[_state] = "committing"; + (async () => { + for (const request of transaction[_requestList]) { + await request[_processedDeferred].promise; + } + core.opSync("op_indexeddb_transaction_commit", transaction[_rid]); // TODO: not sure if the right place + if (transaction[_state] !== "committing") { + return; + } + // TODO: 2.3., 2.4. + + if (transaction[_mode] === "versionchange") { + transaction[_connection][_upgradeTransaction] = null; + } + transaction[_state] = "finished"; + transaction.dispatchEvent(new Event("complete")); + if (transaction[_mode] === "versionchange") { + transaction[_request][_transaction] = null; + } + })(); + } + + const _result = Symbol("[[result]]"); + const _error = Symbol("[[error]]"); + const _source = Symbol("[[source]]"); + const _transaction = Symbol("[[transaction]]"); + const _processedDeferred = Symbol("[[processedDeferred]]"); + const _done = Symbol("[[done]]"); + // Ref: https://w3c.github.io/IndexedDB/#idbrequest + class IDBRequest extends EventTarget { + constructor() { + super(); + webidl.illegalConstructor(); + } + + [_processedDeferred] = new Deferred(); + [_done] = false; + + [_result]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbrequest-result + get result() { + webidl.assertBranded(this, IDBRequestPrototype); + if (!this[_done]) { + throw new DOMException("", "InvalidStateError"); + } + if (this[_error]) { + return undefined; + } else { + return this[_result]; + } + } + + [_error] = null; + get error() { + webidl.assertBranded(this, IDBRequestPrototype); + if (!this[_done]) { + throw new DOMException("", "InvalidStateError"); + } + return this[_error]; + } + + [_source] = null; + get source() { + webidl.assertBranded(this, IDBRequestPrototype); + return this[_source]; + } + + [_transaction] = null; + get transaction() { + webidl.assertBranded(this, IDBRequestPrototype); + return this[_transaction]; + } + + get readyState() { + webidl.assertBranded(this, IDBRequestPrototype); + return this[_done] ? "done" : "pending"; + } + } + defineEventHandler(IDBRequest.prototype, "success"); + defineEventHandler(IDBRequest.prototype, "error"); + + webidl.configurePrototype(IDBRequest); + const IDBRequestPrototype = IDBRequest.prototype; + + // Ref: https://w3c.github.io/IndexedDB/#idbopendbrequest + class IDBOpenDBRequest extends IDBRequest { + constructor() { + super(); + webidl.illegalConstructor(); + } + } + defineEventHandler(IDBOpenDBRequest.prototype, "blocked"); + defineEventHandler(IDBOpenDBRequest.prototype, "upgradeneeded"); + + webidl.configurePrototype(IDBOpenDBRequest); + + /** @type {Map>} */ + const connectionQueue = new Map(); + + // Ref: https://w3c.github.io/IndexedDB/#run-an-upgrade-transaction + /** + * @param connection {IDBDatabase} + * @param version {number} + * @param request {IDBOpenDBRequest} + */ + function runUpgradeTransaction(connection, version, request) { + const transaction = webidl.createBranded(IDBTransaction); + transaction[_mode] = "versionchange"; + transaction[_connection] = connection; + transaction[_scope] = connection[_objectStoreSet]; + connection[_upgradeTransaction] = transaction; + transaction[_state] = "inactive"; + // TODO: 6.: start transaction (call op_indexeddb_transaction_create) + const oldVersion = connection[_version]; + // TODO: 8.: change db version + request[_processedDeferred].resolve(); + + request[_result] = connection; + request[_transaction] = transaction; + request[_done] = true; + transaction[_state] = "active"; + // TODO(@crowlKats): legacyOutputDidListenersThrowFlag + request.dispatchEvent( + new IDBVersionChangeEvent("upgradeneeded", { + bubbles: false, + cancelable: false, + oldVersion, + version, + }), + ); + transaction[_state] = "inactive"; + + // TODO: 11. + } + + // Ref: https://w3c.github.io/IndexedDB/#open-a-database + /** + * @param name {string} + * @param version {number | undefined} + * @param request {IDBOpenDBRequest} + */ + async function openDatabase(name, version, request) { + for (const openRequest of connectionQueue.get(name) ?? []) { + await openRequest[_processedDeferred].promise; + } + connectionQueue.get(name)?.push(request) ?? + connectionQueue.set(name, [request]); + const [newVersion, dbVersion] = core.opSync( + "op_indexeddb_open_database", + name, + version, + ); + const connection = webidl.createBranded(IDBDatabase); + connection[_version] = newVersion; + + if (dbVersion < newVersion) { + // TODO(@crowlKats): 10.1, 10.2, 10.3, 10.4, 10.5: multi-process database connections + runUpgradeTransaction(connection, newVersion, request); + } + return connection; + } + + // Ref: https://w3c.github.io/IndexedDB/#delete-a-database + async function deleteDatabase(name, request) { + for (const openRequest of connectionQueue.get(name) ?? []) { + await openRequest[_processedDeferred].promise; + } + connectionQueue.get(name)?.push(request) ?? + connectionQueue.set(name, [request]); + // TODO: 4.: op to check if db exists + + for (const entry of []) { // TODO: openConnections + if (!entry[_closePending]) { + entry.dispatchEvent( + new IDBVersionChangeEvent("versionchange", { + bubbles: false, + cancelable: false, + oldVersion: "", // TODO: db's version from 4. + newVersion: null, + }), + ); + } + } + for (const entry of []) { // TODO: openConnections + if (!entry[_closePending]) { + request.dispatchEvent( + new IDBVersionChangeEvent("blocked", { + bubbles: false, + cancelable: false, + oldVersion: "", // TODO: db's version from 4. + newVersion: null, + }), + ); + break; + } + } + + // TODO: 11.: op to delete db + // TODO: 12.: return db's version from 4. + } + + // Ref: https://w3c.github.io/IndexedDB/#idbfactory + class IDBFactory { + constructor() { + webidl.illegalConstructor(); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbfactory-open + open(name, version = undefined) { + webidl.assertBranded(this, IDBFactoryPrototype); + const prefix = "Failed to execute 'open' on 'IDBFactory'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + name = webidl.converters.DOMString(name, { + prefix, + context: "Argument 1", + }); + if (version !== undefined) { + version = webidl.converters["unsigned long long"](version, { + prefix, + context: "Argument 2", + enforceRange: true, + }); + } + + if (version === 0) { + throw new TypeError(); + } + + const request = webidl.createBranded(IDBOpenDBRequest); + + (async () => { + try { + const res = await openDatabase(name, version, request); + request[_result] = res; + request[_done] = true; + request.dispatchEvent(new Event("success")); + } catch (e) { + request[_result] = undefined; + request[_error] = e; + request[_done] = true; + request.dispatchEvent( + new Event("error", { + bubbles: true, + cancelable: true, + }), + ); + } + })(); + + return request; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbfactory-deletedatabase + deleteDatabase(name) { + webidl.assertBranded(this, IDBFactoryPrototype); + const prefix = "Failed to execute 'deleteDatabase' on 'IDBFactory'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + name = webidl.converters.DOMString(name, { + prefix, + context: "Argument 1", + }); + + const request = webidl.createBranded(IDBOpenDBRequest); + + (async () => { + try { + const res = await deleteDatabase(name, request); + request[_processedDeferred].resolve(); + request[_result] = undefined; + request[_done] = true; + request.dispatchEvent( + new IDBVersionChangeEvent("success", { + bubbles: false, + cancelable: false, + oldVersion: res, + newVersion: null, + }), + ); + } catch (e) { + request[_processedDeferred].resolve(); + request[_error] = e; + request[_done] = true; + request.dispatchEvent( + new Event("error", { + bubbles: true, + cancelable: true, + }), + ); + } + })(); + + return request; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbfactory-databases + databases() { + webidl.assertBranded(this, IDBFactoryPrototype); + return core.opAsync("op_indexeddb_list_databases"); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbfactory-cmp + cmp(first, second) { + webidl.assertBranded(this, IDBFactoryPrototype); + const prefix = "Failed to execute 'cmp' on 'IDBFactory'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + first = webidl.converters.any(first, { + prefix, + context: "Argument 1", + }); + + second = webidl.converters.any(second, { + prefix, + context: "Argument 2", + }); + + const a = valueToKey(first); + if (a === null) { + throw new DOMException( + "Data provided does not meet requirements", + "DataError", + ); + } + const b = valueToKey(second); + if (b === null) { + throw new DOMException( + "Data provided does not meet requirements", + "DataError", + ); + } + + return compareTwoKeys(a, b); + } + } + webidl.configurePrototype(IDBFactory); + const IDBFactoryPrototype = IDBFactory.prototype; + + class Database { + /** @type {number} */ + version; + /** @type {string} */ + name; + } + + /** @type {Set} */ + const connections = new Set(); + + const _name = Symbol("[[name]]"); + const _version = Symbol("[[version]]"); + const _objectStores = Symbol("[[objectStores]]"); + const _upgradeTransaction = Symbol("[[upgradeTransaction]]"); + const _db = Symbol("[[db]]"); + const _closePending = Symbol("[[closePending]]"); + const _objectStoreSet = Symbol("[[objectStoreSet]]"); + const _close = Symbol("[[close]]"); + const _transactions = Symbol("[[transactions]]"); + // Ref: https://w3c.github.io/IndexedDB/#idbdatabase + // TODO: finalizationRegistry: If an IDBDatabase object is garbage collected, the associated connection must be closed. + class IDBDatabase extends EventTarget { + /** @type {Set} */ + [_objectStores] = new Set(); + /** @type {(IDBTransaction | null)} */ + [_upgradeTransaction] = null; + /** @type {Database} */ + [_db]; + /** @type {boolean} */ + [_closePending] = false; + /** @type {IDBTransaction[]} */ + [_transactions] = []; + + /** @type {Map} */ + [_objectStoreSet]; // TODO: update on upgrade transaction + + constructor() { + super(); + webidl.illegalConstructor(); + } + + [_name]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-name + get name() { + webidl.assertBranded(this, IDBDatabasePrototype); + return this[_name]; + } + + [_version]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-version + get version() { + webidl.assertBranded(this, IDBDatabasePrototype); + return this[_version]; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-objectstorenames + get objectStoreNames() { + webidl.assertBranded(this, IDBDatabasePrototype); + return ArrayPrototypeSort([ + ...new SafeArrayIterator( + MapPrototypeKeys(this[_objectStoreSet]), + ), + ]); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-transaction + transaction(storeNames, mode = "readonly", options = {}) { + webidl.assertBranded(this, IDBDatabasePrototype); + const prefix = "Failed to execute 'transaction' on 'IDBDatabase'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + storeNames = webidl.converters["sequence or DOMString"]( + storeNames, + { + prefix, + context: "Argument 1", + }, + ); + mode = webidl.converters.IDBTransactionMode(mode, { + prefix, + context: "Argument 2", + }); + options = webidl.converters.IDBTransactionOptions(options, { + prefix, + context: "Argument 3", + }); + + if (this[_connection][_closePending]) { + throw new DOMException("", "InvalidStateError"); + } + const scope = new Set( + ArrayIsArray(storeNames) ? storeNames : [storeNames], + ); + // TODO: 4.: should this be an op? should the names be cached? + if (scope.size === 0) { + throw new DOMException("", "InvalidAccessError"); + } + if (mode !== "readonly" && mode !== "readwrite") { + throw new TypeError(""); + } + const rid = core.opSync("op_indexeddb_transaction_create"); + const transaction = webidl.createBranded(IDBTransaction); + transaction[_connection] = this; + transaction[_rid] = rid; + transaction[_mode] = mode; + transaction[_durabilityHint] = options.durability; + // TODO: scope: get all stores and filter keep only ones in scope & assign to transaction[_scope] + this[_transactions].push(transaction); + return transaction; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-close + close() { + webidl.assertBranded(this, IDBDatabasePrototype); + this[_close](false); + } + + /** + * @param forced {boolean} + */ + // Ref: https://w3c.github.io/IndexedDB/#close-a-database-connection + [_close](forced) { + this[_closePending] = true; + if (forced) { + for (const transaction of this[_transactions]) { + abortTransaction(transaction, new DOMException("", "AbortError")); + } + } + for (const transaction of this[_transactions]) { + // TODO: 3.: wait for all transactions to complete. this needs to be sync, but the requested action is inherently async + } + if (forced) { + this.dispatchEvent(new Event("close")); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore + createObjectStore(name, options = {}) { + webidl.assertBranded(this, IDBDatabasePrototype); + const prefix = "Failed to execute 'createObjectStore' on 'IDBDatabase'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + name = webidl.converters.DOMString(name, { + prefix, + context: "Argument 1", + }); + options = webidl.converters.IDBObjectStoreParameters(options, { + prefix, + context: "Argument 2", + }); + + if (this[_upgradeTransaction] === null) { + throw new DOMException( + "No upgrade transaction present", + "InvalidStateError", + ); + } + + if (this[_upgradeTransaction][_state] !== "active") { + throw new DOMException( + "Upgrade transaction is not active", + "TransactionInactiveError", + ); + } + + const keyPath = options.keyPath ?? null; + + if (options.keyPath !== null && !isValidKeyPath(options.keyPath)) { + throw new DOMException("", "SyntaxError"); + } + + if ( + options.autoIncrement && + ((typeof options.keyPath === "string" && + options.keyPath.length === 0) || + ArrayIsArray(options.keyPath)) + ) { + throw new DOMException("", "InvalidAccessError"); + } + + core.opSync( + "op_indexeddb_database_create_object_store", + this[_name], + name, + keyPath, + ); + + const store = new Store(options.autoIncrement); + store.name = name; + store.database = this; + store.keyPath = keypath; + const objectStore = webidl.createBranded(IDBObjectStore); + objectStore[_name] = name; + objectStore[_store] = store; + objectStore[_transaction] = this[_upgradeTransaction]; + return objectStore; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-deleteobjectstore + deleteObjectStore(name) { + webidl.assertBranded(this, IDBDatabasePrototype); + const prefix = "Failed to execute 'deleteObjectStore' on 'IDBDatabase'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + name = webidl.converters.DOMString(name, { + prefix, + context: "Argument 1", + }); + + if (this[_upgradeTransaction] === null) { + throw new DOMException("", "InvalidStateError"); + } + + if (this[_upgradeTransaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + + const store = MapPrototypeGet(this[_objectStoreSet], name); + if (store === undefined) { + throw new DOMException("", "NotFoundError"); + } + MapPrototypeDelete(this[_objectStoreSet], name); + core.opSync("op_indexeddb_database_delete_object_store", this[_name], store[_name]); + // TODO 6.: ops + } + } + defineEventHandler(IDBDatabase.prototype, "abort"); + defineEventHandler(IDBDatabase.prototype, "close"); + defineEventHandler(IDBDatabase.prototype, "error"); + defineEventHandler(IDBDatabase.prototype, "versionchange"); + + webidl.configurePrototype(IDBDatabase); + const IDBDatabasePrototype = IDBDatabase.prototype; + + // Ref: https://w3c.github.io/IndexedDB/#object-store-construct + class Store { + /** @type {string} */ + name; + /** @type {IDBDatabase} */ + database; + + keyPath; // TODO: should this be here? or somewhere else? + + /** @type {null | KeyGenerator} */ + keyGenerator = null; + + constructor(generator) { + if (generator) { + // Ref: https://w3c.github.io/IndexedDB/#key-generator-construct + this.keyGenerator = { + current: 1, + // Ref: https://w3c.github.io/IndexedDB/#generate-a-key + generateKey() { + if (this.current > 9007199254740992) { + throw new DOMException("", "ConstraintError"); + } + return { + type: "number", + value: this.current++, + }; + }, + // Ref: https://w3c.github.io/IndexedDB/#possibly-update-the-key-generator + possiblyUpdate(key) { + if (key.type !== "number") { + return; + } + const value = MathFloor(MathMin(key.value, 9007199254740992)); + if (value >= this.current) { + this.current = value + 1; + } + }, + }; + } + } + } + + // Ref: https://w3c.github.io/IndexedDB/#store-a-record-into-an-object-store + function storeRecordIntoObjectStore(store, value, key, noOverwrite) { + if (store.keyGenerator !== null) { + if (key === undefined) { + key = store.keyGenerator.generateKey(); + if (store.keyPath !== null) { + injectKeyIntoValueUsingKeyPath(value, key, store.keyPath); + } + } else { + store.keyGenerator.possiblyUpdate(key); + } + } + + const indexes = core.opSync( + "op_indexeddb_object_store_add_or_put_records", + store.database.name, + store.name, + core.deserialize(value), + key, + noOverwrite, + ); + + for (const index of indexes) { + let indexKey; + try { + indexKey = extractKeyFromValueUsingKeyPath( + value, + index.keyPath, + index.multiEntry, + ); + if (indexKey === null || indexKey === _failure) { + continue; + } + } catch (e) {} + core.opSync( + "op_indexeddb_object_store_add_or_put_records_handle_index", + index, + indexKey, + ); + } + + return key; + } + + /** + * @param store {IDBObjectStore} + */ + function assertObjectStore(store) { + return core.opSync( + "op_indexeddb_object_store_exists", + store[_store].database[_name], + store[_store].name, + ); + } + + /** + * @param index {IDBIndex} + */ + function assertIndex(index) { + assertObjectStore(index[_storeHandle]); + return core.opSync( + "op_indexeddb_index_exists", + index[_storeHandle][_store].database[_name], + index[_storeHandle][_store].name, + index[_index].name, + ); + } + + /** + * @param cursor {IDBCursor} + */ + function assertCursor(cursor) { + assertObjectStore(cursor[_effectiveObjectStore]); + if (cursor[_source] instanceof IDBObjectStore) { + assertObjectStore(cursor[_source]); + } else { + assertIndex(cursor[_source]); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#add-or-put + /** + * @param handle {IDBObjectStore} + * @param value {any} + * @param key {any} + * @param noOverwrite {boolean} + */ + function addOrPut(handle, value, key, noOverwrite) { + assertObjectStore(handle); + + if (handle[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + + if (handle[_transaction][_mode] !== "readonly") { + throw new DOMException("", "ReadOnlyError"); + } + + if (handle[_store].keyPath !== null && key !== undefined) { + throw new DOMException("", "DataError"); + } + + if ( + handle[_store].keyPath === null && handle[_store].keyGenerator === null && + key === undefined + ) { + throw new DOMException("", "DataError"); + } + + if (key !== undefined) { + const r = valueToKey(key); + if (r === null) { + throw new DOMException("", "DataError"); + } + key = r; + } + const cloned = clone(handle[_transaction], value); + + if (handle[_store].keyPath !== null) { + const kpk = extractKeyFromValueUsingKeyPath( + cloned, + handle[_store].keyPath, + ); + if (kpk === null) { + throw new DOMException("", "DataError"); + } + if (kpk !== _failure) { + key = kpk; + } else { + if (handle[_store].keyGenerator === null) { + throw new DOMException("", "DataError"); + } else { + if (!checkKeyCanBeInjectedIntoValue(cloned, handle[_store].keyPath)) { + throw new DOMException("", "DataError"); + } + } + } + } + + return asynchronouslyExecuteRequest( + handle, + () => + storeRecordIntoObjectStore(handle[_store], cloned, key, noOverwrite), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#delete-records-from-an-object-store + function deleteRecordsFromObjectStore(store, range) { + core.opSync( + "op_indexeddb_object_store_delete_records", + store.database.name, + store.name, + range, + ); + return undefined; + } + + // Ref: https://w3c.github.io/IndexedDB/#clear-an-object-store + function clearObjectStore(store) { + core.opSync( + "op_indexeddb_object_store_clear", + store.database.name, + store.name, + ); + return undefined; + } + + // Ref: https://w3c.github.io/IndexexdDB/#retrieve-a-value-from-an-object-store + function retrieveValueFromObjectStore(store, range) { + const val = core.opSync( + "op_indexeddb_object_store_retrieve_value", + store.database.name, + store.name, + range, + ); + if (val === null) { + return undefined; + } else { + return core.deserialize(val); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-values-from-an-object-store + function retrieveMultipleValuesFromObjectStore(store, range, count) { + const vals = core.opSync( + "op_indexeddb_object_store_retrieve_multiple_values", + store.database.name, + store.name, + range, + count, + ); + return vals.map((val) => core.deserialize(val)); + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-key-from-an-object-store + function retrieveKeyFromObjectStore(store, range) { + const val = core.opSync( + "op_indexeddb_object_store_retrieve_key", + store.database.name, + store.name, + range, + ); + if (val === null) { + return undefined; + } else { + return keyToValue(val); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-keys-from-an-object-store + function retrieveMultipleKeysFromObjectStore(store, range, count) { + const vals = core.opSync( + "op_indexeddb_object_store_retrieve_multiple_keys", + store.database.name, + store.name, + range, + count, + ); + return vals.map((val) => keyToValue(val)); + } + + // Ref: https://w3c.github.io/IndexedDB/#count-the-records-in-a-range + function countRecordsInRange(storeOrIndex, range) { + if (storeOrIndex instanceof Store) { + return core.opSync( + "op_indexeddb_object_store_count_records", + storeOrIndex.database.name, + storeOrIndex.name, + range, + ); + } else { + assert(storeOrIndex instanceof Index); + return core.opSync( + "op_indexeddb_object_store_count_records", + storeOrIndex.database.name, + storeOrIndex.name, + range, + ); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#iterate-a-cursor + function iterateCursor(cursor, key, primaryKey, count = 1) { + if (primaryKey !== undefined) { + assert( + cursor[_source] instanceof IDBIndex && + (cursor[_direction] === "next" || cursor[_direction] === "prev"), + ); + } + /** @type {[Key, Uint8Array][]} */ + const records = cursor[_source] instanceof IDBObjectStore + ? core.opSync( + "op_indexeddb_object_store_get_records", + cursor[_source][_store].database[_name], + cursor[_source][_name], + ) + : core.opSync("op_indexeddb_index_get_records"); // TODO + let position = cursor[_position]; + let objectStorePosition = cursor[_objectStorePosition]; + + // TODO: check: we call valueToKey, but the spec never says to do that, but references key comparison. + let foundRecord = undefined; + for (; count > 0; count--) { + switch (cursor[_direction]) { + case "next": { + foundRecord = records.find(([recordKey, value]) => { + let a = true; + if (key !== undefined) { + a = compareTwoKeys(recordKey, key) !== -1; + } + + let b = true; + if (primaryKey !== undefined) { + b = + (compareTwoKeys(recordKey, key) === 0 && + compareTwoKeys( + valueToKey(core.deserialize(value)), + primaryKey, + ) !== -1) || compareTwoKeys(recordKey, key) === 1; + } + + let c = true; + if (position !== undefined) { + if (cursor[_source] instanceof IDBObjectStore) { + c = compareTwoKeys(recordKey, position) === 1; + } else { + c = + (compareTwoKeys(recordKey, position) === 0 && + compareTwoKeys( + valueToKey(core.deserialize(value)), + valueToKey(cursor[_objectStorePosition]), + ) === 1) || compareTwoKeys(recordKey, position) === 1; + } + } + + return a && b && c && keyInRange(cursor[_range], recordKey); + }); + break; + } + case "nextunique": { + foundRecord = records.find(([recordKey, value]) => { + let a = true; + if (key !== undefined) { + a = compareTwoKeys(recordKey, key) !== -1; + } + + let b = true; + if (position !== undefined) { + b = compareTwoKeys(recordKey, position) === 1; + } + + return a && b && keyInRange(cursor[_range], recordKey); + }); + break; + } + case "prev": { + for (let i = records.length - 1; i >= 0; i--) { + const [recordKey, value] = records[i]; + let a = true; + if (key !== undefined) { + a = compareTwoKeys(recordKey, key) !== 1; + } + + let b = true; + if (primaryKey !== undefined) { + b = + (compareTwoKeys(recordKey, key) === 0 && + compareTwoKeys( + valueToKey(core.deserialize(value)), + primaryKey, + ) !== 1) || compareTwoKeys(recordKey, key) === -1; + } + + let c = true; + if (position !== undefined) { + if (cursor[_source] instanceof IDBObjectStore) { + c = compareTwoKeys(recordKey, position) === -1; + } else { + c = + (compareTwoKeys(recordKey, position) === 0 && + compareTwoKeys( + valueToKey(core.deserialize(value)), + valueToKey(cursor[_objectStorePosition]), + ) === -1) || compareTwoKeys(recordKey, position) === -1; + } + } + + if (a && b && c && keyInRange(cursor[_range], recordKey)) { + foundRecord = records[i]; + break; + } + } + break; + } + case "prevunique": { + let tempRecord = undefined; + for (let i = records.length - 1; i >= 0; i--) { + const [recordKey, value] = records[i]; + let a = true; + if (key !== undefined) { + a = compareTwoKeys(recordKey, key) !== 1; + } + + let b = true; + if (position !== undefined) { + b = compareTwoKeys(recordKey, position) === -1; + } + + if (a && b && keyInRange(cursor[_range], recordKey)) { + tempRecord = records[i]; + break; + } + } + + if (tempRecord !== undefined) { + foundRecord = records.find(([recordKey, value]) => { + return compareTwoKeys(recordKey, tempRecord[0]) === 0; + }); + } + break; + } + } + + if (foundRecord === undefined) { + if (cursor[_source] instanceof IDBIndex) { + cursor[_objectStorePosition] = undefined; + } + if (!cursor[_keyOnly]) { + cursor[_value] = undefined; + } + return null; + } + + position = foundRecord[0]; + if (cursor[_source] instanceof IDBIndex) { + objectStorePosition = core.deserialize(foundRecord[1]); + } + } + + cursor[_position] = position; + if (cursor[_source] instanceof IDBIndex) { + cursor[_objectStorePosition] = objectStorePosition; + } + cursor[_key] = foundRecord[0]; + if (!cursor[_keyOnly]) { + // TODO: referencedValue: investigate it, and replace normal value usages for it where appropriate. + cursor[_value] = core.deserialize(foundRecord[1]); + } + cursor[_gotValue] = true; + return cursor; + } + + const _keyPath = Symbol("[[keyPath]]"); + const _store = Symbol("[[store]]"); + const _indexSet = Symbol("[[indexSet]]"); + // Ref: https://w3c.github.io/IndexedDB/#idbobjectstore + class IDBObjectStore { + constructor() { + webidl.illegalConstructor(); + } + + /** @type {IDBTransaction} */ + [_transaction]; + /** @type {Store} */ + [_store]; + /** @type {Map} */ + [_indexSet]; // TODO: set + + /** @type {string} */ + [_name]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-name + get name() { + webidl.assertBranded(this, IDBObjectStorePrototype); + return this[_name]; + } + + // Ref: https://w3c.github.io/IndexedDB/#ref-for-dom-idbobjectstore-name%E2%91%A2 + set name(name) { + webidl.assertBranded(this, IDBObjectStorePrototype); + name = webidl.converters.DOMString(name, { + prefix: "Failed to set 'name' on 'IDBObjectStore'", + context: "Argument 1", + }); + assertObjectStore(this); + if (this[_transaction][_mode] !== "versionchange") { + throw new DOMException("", "InvalidStateError"); + } + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + if (this[_store][_name] === name) { + return; + } + core.opSync( + "op_indexeddb_object_store_rename", + this[_store].database.name, + this[_name], + name, + ); + this[_store].name = name; + this[_name] = name; + } + + [_keyPath]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-keypath + get keyPath() { + webidl.assertBranded(this, IDBObjectStorePrototype); + return this[_keyPath]; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-indexnames + get indexNames() { + webidl.assertBranded(this, IDBObjectStorePrototype); + return [...this[_indexSet].keys()]; + } + + [_transaction]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-transaction + get transaction() { + webidl.assertBranded(this, IDBObjectStorePrototype); + return this[_transaction]; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-autoincrement + get autoIncrement() { + webidl.assertBranded(this, IDBObjectStorePrototype); + return this[_store].keyGenerator !== null; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-put + put(value, key) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'put' on 'IDBObjectStore'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + value = webidl.converters.any(value, { + prefix, + context: "Argument 1", + }); + key = webidl.converters.any(key, { + prefix, + context: "Argument 2", + }); + + return addOrPut(this, value, key, false); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-add + add(value, key) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'add' on 'IDBObjectStore'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + value = webidl.converters.any(value, { + prefix, + context: "Argument 1", + }); + key = webidl.converters.any(key, { + prefix, + context: "Argument 2", + }); + + return addOrPut(this, value, key, true); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-delete + delete(query) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'delete' on 'IDBObjectStore'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + if (this[_transaction][_mode] === "readonly") { + throw new DOMException("", "ReadOnlyError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => deleteRecordsFromObjectStore(this[_store], range), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-clear + clear() { + webidl.assertBranded(this, IDBObjectStorePrototype); + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + if (this[_transaction][_mode] === "readonly") { + throw new DOMException("", "ReadOnlyError"); + } + return asynchronouslyExecuteRequest( + this, + () => clearObjectStore(this[_store]), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-get + get(query) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'get' on 'IDBObjectStore'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveValueFromObjectStore(this[_store], range), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-getkey + getKey(query) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'getKey' on 'IDBObjectStore'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveKeyFromObjectStore(this[_store], range), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-getall + getAll(query, count = undefined) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'getAll' on 'IDBObjectStore'"; + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + if (count !== undefined) { + count = webidl.converters["unsigned long"](count, { + prefix, + context: "Argument 2", + enforceRange: true, + }); + } + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveMultipleValuesFromObjectStore(this[_store], range, count), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-getallkeys + getAllKeys(query, count = undefined) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'getAllKeys' on 'IDBObjectStore'"; + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + if (count !== undefined) { + count = webidl.converters["unsigned long"](count, { + prefix, + context: "Argument 2", + enforceRange: true, + }); + } + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveMultipleKeysFromObjectStore(this[_store], range, count), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-count + count(query) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'count' on 'IDBObjectStore'"; + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => countRecordsInRange(this[_store], range), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-opencursor + openCursor(query, direction = "next") { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'openCursor' on 'IDBObjectStore'"; + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + direction = webidl.converters.IDBCursorDirection(direction, { + prefix, + context: "Argument 2", + }); + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + const cursor = createCursor( + this[_transaction], + direction, + this, + range, + false, + ); + const request = asynchronouslyExecuteRequest( + this, + () => iterateCursor(cursor), + ); + cursor[_request] = request; + return request; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-openkeycursor + openKeyCursor(query, direction = "next") { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'openKeyCursor' on 'IDBObjectStore'"; + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + direction = webidl.converters.IDBCursorDirection(direction, { + prefix, + context: "Argument 2", + }); + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + const cursor = createCursor( + this[_transaction], + direction, + this, + range, + true, + ); + const request = asynchronouslyExecuteRequest( + this, + () => iterateCursor(cursor), + ); + cursor[_request] = request; + return request; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-index + index(name) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'index' on 'IDBObjectStore'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + name = webidl.converters.DOMString(name, { + prefix, + context: "Argument 1", + }); + assertObjectStore(this); + if (this[_transaction][_state] === "finished") { + throw new DOMException("", "InvalidStateError"); + } + const index = this[_indexSet].get(name); + if (index === undefined) { + throw new DOMException("", "NotFoundError"); + } + const indexHandle = webidl.createBranded(IDBIndex); + indexHandle[_index] = index; + indexHandle[_storeHandle] = this; + return indexHandle; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-createindex + createIndex(name, keyPath, options = {}) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'createIndex' on 'IDBObjectStore'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + name = webidl.converters.DOMString(name, { + prefix, + context: "Argument 1", + }); + keyPath = webidl.converters["sequence or DOMString"](keyPath, { + prefix, + context: "Argument 2", + }); + options = webidl.converters.IDBIndexParameters(options, { + prefix, + context: "Argument 3", + }); + if (this[_transaction][_mode] !== "versionchange") { + throw new DOMException("", "InvalidStateError"); + } + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + // TODO: 6.: op? since we have indexset, we should check that. if it isnt reliable, then whats the point of the cache? + + if (!isValidKeyPath(keyPath)) { + throw new DOMException("", "SyntaxError"); + } + if (ArrayIsArray(keyPath) && options.multiEntry) { + throw new DOMException("", "InvalidAccessError"); + } + // TODO: 11.: ops + const index = new Index(); + index.name = name; + index.multiEntry = options.multiEntry; + index.unique = options.unique; + this[_indexSet].set(name, index); + const indexHandle = webidl.createBranded(IDBIndex); + indexHandle[_index] = index; + indexHandle[_storeHandle] = this; + return indexHandle; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-deleteindex + deleteIndex(name) { + webidl.assertBranded(this, IDBObjectStorePrototype); + const prefix = "Failed to execute 'deleteIndex' on 'IDBObjectStore'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + name = webidl.converters.DOMString(name, { + prefix, + context: "Argument 1", + }); + if (this[_transaction][_mode] !== "versionchange") { + throw new DOMException("", "InvalidStateError"); + } + assertObjectStore(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + // TODO: 6., 7., 8.: op + } + } + webidl.configurePrototype(IDBObjectStore); + const IDBObjectStorePrototype = IDBObjectStore.prototype; + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-referenced-value-from-an-index + /** + * @param index {IDBIndex} + * @param range {IDBKeyRange} + */ + function retrieveReferencedValueFromIndex(index, range) { + const val = core.opSync( + "op_indexeddb_index_retrieve_value", + index[_storeHandle][_transaction][_connection][_name], + index[_storeHandle][_store][_name], + index[_index][_name], + range, + ); + if (val === null) { + return undefined; + } else { + return core.deserialize(val); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-referenced-values-from-an-index + function retrieveMultipleReferencedValuesFromIndex(index, range, count) { + const vals = core.opSync( + "op_indexeddb_index_retrieve_multiple_values", + index[_storeHandle][_transaction][_connection][_name], + index[_storeHandle][_store][_name], + index[_index][_name], + range, + count, + ); + return vals.map((val) => core.deserialize(val)); + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-index + function retrieveValueFromIndex(index, range) { + const val = core.opSync( + "op_indexeddb_index_retrieve_value", + index[_storeHandle][_transaction][_connection][_name], + index[_storeHandle][_store][_name], + index[_index][_name], + range, + ); + if (val === undefined) { + return undefined; + } else { + return keyToValue(val); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-index + function retrieveMultipleValuesFromIndex(index, range, count) { + const vals = core.opSync( + "op_indexeddb_index_retrieve_multiple_values", + index[_storeHandle][_transaction][_connection][_name], + index[_storeHandle][_store][_name], + index[_index][_name], + range, + count, + ); + return vals.map((val) => keyToValue(val)); + } + + class Index { + /** @type {string} */ + name; + /** @type {boolean} */ + multiEntry; + /** @type {boolean} */ + unique; + } + + const _index = Symbol("[[_index]]"); + const _storeHandle = Symbol("[[storeHandle]]"); + // Ref: https://w3c.github.io/IndexedDB/#idbindex + class IDBIndex { + constructor() { + webidl.illegalConstructor(); + } + + /** @type {Index} */ + [_index]; + /** @type {IDBObjectStore} */ + [_storeHandle]; + + [_name]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-name + get name() { + webidl.assertBranded(this, IDBIndexPrototype); + return this[_name]; + } + + // Ref: https://w3c.github.io/IndexedDB/#ref-for-dom-idbindex-name%E2%91%A2 + set name(name) { + webidl.assertBranded(this, IDBIndexPrototype); + name = webidl.converters.DOMString(name, { + prefix: "Failed to set 'name' on 'IDBIndex'", + context: "Argument 1", + }); + + if (this[_transaction][_mode] !== "versionchange") { + throw new DOMException("", "InvalidStateError"); + } + + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + + assertIndex(this); + + // TODO: 7.: should it be this's _name? or this's _index's name + // TODO: 8.: cache + + this[_index].name = name; + this[_name] = name; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-objectstore + get objectStore() { + webidl.assertBranded(this, IDBIndexPrototype); + return this[_storeHandle]; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-keypath + get keyPath() { + webidl.assertBranded(this, IDBIndexPrototype); + return this[_storeHandle][_store].keyPath; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-multientry + get multiEntry() { + webidl.assertBranded(this, IDBIndexPrototype); + return this[_index].multiEntry; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-unique + get unique() { + webidl.assertBranded(this, IDBIndexPrototype); + return this[_index].unique; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-get + get(query) { + webidl.assertBranded(this, IDBIndexPrototype); + const prefix = "Failed to execute 'get' on 'IDBIndex'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + assertIndex(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveReferencedValueFromIndex(this, range), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-getkey + getKey(query) { + webidl.assertBranded(this, IDBIndexPrototype); + const prefix = "Failed to execute 'getKey' on 'IDBIndex'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + assertIndex(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveValueFromIndex(this, range), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-getall + getAll(query, count = undefined) { + webidl.assertBranded(this, IDBIndexPrototype); + const prefix = "Failed to execute 'getAll' on 'IDBIndex'"; + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + if (count !== undefined) { + count = webidl.converters["unsigned long"](count, { + prefix, + context: "Argument 2", + enforceRange: true, + }); + } + assertIndex(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveMultipleReferencedValuesFromIndex(this, range, count), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-getallkeys + getAllKeys(query, count = undefined) { + webidl.assertBranded(this, IDBIndexPrototype); + const prefix = "Failed to execute 'getAllKeys' on 'IDBIndex'"; + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + if (count !== undefined) { + count = webidl.converters["unsigned long"](count, { + prefix, + context: "Argument 2", + enforceRange: true, + }); + } + assertIndex(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveMultipleValuesFromIndex(this, range, count), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-count + count(query) { + webidl.assertBranded(this, IDBIndexPrototype); + const prefix = "Failed to execute 'count' on 'IDBIndex'"; + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + assertIndex(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => countRecordsInRange(this[_index], range), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-opencursor + openCursor(query, direction = "next") { + webidl.assertBranded(this, IDBIndexPrototype); + const prefix = "Failed to execute 'openCursor' on 'IDBIndex'"; + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + direction = webidl.converters.IDBCursorDirection(direction, { + prefix, + context: "Argument 2", + }); + assertIndex(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + const cursor = createCursor( + this[_transaction], + direction, + this, + range, + false, + ); + const request = asynchronouslyExecuteRequest( + this, + () => iterateCursor(cursor), + ); + cursor[_request] = request; + return request; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-openkeycursor + openKeyCursor(query, direction = "next") { + webidl.assertBranded(this, IDBIndexPrototype); + const prefix = "Failed to execute 'openKeyCursor' on 'IDBIndex'"; + query = webidl.converters.any(query, { + prefix, + context: "Argument 1", + }); + direction = webidl.converters.IDBCursorDirection(direction, { + prefix, + context: "Argument 2", + }); + assertIndex(this); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + const cursor = createCursor( + this[_transaction], + direction, + this, + range, + true, + ); + const request = asynchronouslyExecuteRequest( + this, + () => iterateCursor(cursor), + ); + cursor[_request] = request; + return request; + } + } + webidl.configurePrototype(IDBIndex); + const IDBIndexPrototype = IDBIndex.prototype; + + const _lowerBound = Symbol("[[lowerBound]]"); + const _upperBound = Symbol("[[upperBound]]"); + const _lowerOpen = Symbol("[[lowerOpen]]"); + const _upperOpen = Symbol("[[upperOpen]]"); + + function createRange( + lowerBound, + upperBound, + lowerOpen = false, + upperOpen = false, + ) { + const range = webidl.createBranded(IDBKeyRange); + range[_lowerBound] = lowerBound; + range[_upperBound] = upperBound; + range[_lowerOpen] = lowerOpen; + range[_upperOpen] = upperOpen; + return range; + } + + /** + * @param range {IDBKeyRange} + * @param key {any} + * @returns {boolean} + */ + // Ref: https://w3c.github.io/IndexedDB/#in + function keyInRange(range, key) { + const lower = range[_lowerBound] === null || + compareTwoKeys(range[_lowerBound], key) === -1 || + (compareTwoKeys(range[_lowerBound], key) === 0 && !range[_lowerOpen]); + const upper = range[_upperBound] === null || + compareTwoKeys(range[_upperBound], key) === 1 || + (compareTwoKeys(range[_upperBound], key) === 0 && !range[_upperOpen]); + return lower && upper; + } + + // Ref: https://w3c.github.io/IndexedDB/#idbkeyrange + class IDBKeyRange { + constructor() { + webidl.illegalConstructor(); + } + + [_lowerBound]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-lower + get lower() { + webidl.assertBranded(this, IDBKeyRangePrototype); + return this[_lowerBound]; + } + + [_upperBound]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-upper + get upper() { + webidl.assertBranded(this, IDBKeyRangePrototype); + return this[_upperBound]; + } + + [_lowerOpen]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-loweropen + get lowerOpen() { + webidl.assertBranded(this, IDBKeyRangePrototype); + return this[_lowerOpen]; + } + + [_upperOpen]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-upperopen + get upperOpen() { + webidl.assertBranded(this, IDBKeyRangePrototype); + return this[_upperOpen]; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-only + static only(value) { + const prefix = "Failed to execute 'only' on 'IDBKeyRange'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + value = webidl.converters.any(value, { + prefix, + context: "Argument 1", + }); + const key = valueToKey(value); + if (key === null) { + throw new DOMException("Invalid key provided", "DataError"); + } + return createRange(key, key); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-lowerbound + static lowerBound(lower, open = false) { + const prefix = "Failed to execute 'lowerBound' on 'IDBKeyRange'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + lower = webidl.converters.any(lower, { + prefix, + context: "Argument 1", + }); + open = webidl.converters.boolean(open, { + prefix, + context: "Argument 2", + }); + const lowerKey = valueToKey(lower); + if (lowerKey === null) { + throw new DOMException("Invalid key provided", "DataError"); + } + return createRange(lowerKey, null, open, true); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-upperbound + static upperBound(upper, open = false) { + const prefix = "Failed to execute 'upperBound' on 'IDBKeyRange'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + upper = webidl.converters.any(upper, { + prefix, + context: "Argument 1", + }); + open = webidl.converters.boolean(open, { + prefix, + context: "Argument 2", + }); + const upperKey = valueToKey(upper); + if (upperKey === null) { + throw new DOMException("Invalid key provided", "DataError"); + } + return createRange(null, upperKey, true, open); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-bound + static bound(lower, upper, lowerOpen = false, upperOpen = false) { + const prefix = "Failed to execute 'bound' on 'IDBKeyRange'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + lower = webidl.converters.any(lower, { + prefix, + context: "Argument 1", + }); + upper = webidl.converters.any(upper, { + prefix, + context: "Argument 2", + }); + lowerOpen = webidl.converters.boolean(lowerOpen, { + prefix, + context: "Argument 3", + }); + upperOpen = webidl.converters.boolean(upperOpen, { + prefix, + context: "Argument 4", + }); + const lowerKey = valueToKey(lower); + if (lowerKey === null) { + throw new DOMException("Invalid lower key provided", "DataError"); + } + const upperKey = valueToKey(upper); + if (upperKey === null) { + throw new DOMException("Invalid upper key provided", "DataError"); + } + if (compareTwoKeys(lowerKey, upperKey) === 1) { + throw new DOMException( + "Lower key is greater than upper key", + "DataError", + ); + } + return createRange(lowerKey, upperKey, lowerOpen, upperOpen); + } + + includes(key) { + webidl.assertBranded(this, IDBKeyRangePrototype); + const prefix = "Failed to execute 'includes' on 'IDBKeyRange'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + key = webidl.converters.any(key, { + prefix, + context: "Argument 1", + }); + const keyVal = valueToKey(key); + if (keyVal === null) { + throw new DOMException("Invalid key provided", "DataError"); + } + return keyInRange(this, key); + } + } + webidl.configurePrototype(IDBKeyRange); + const IDBKeyRangePrototype = IDBKeyRange.prototype; + + function createCursor(transaction, direction, source, range, keyOnly) { + const cursor = webidl.createBranded(IDBCursor); + cursor[_transaction] = transaction; + cursor[_position] = undefined; + cursor[_direction] = direction; + cursor[_gotValue] = false; + cursor[_key] = undefined; + cursor[_value] = undefined; + cursor[_source] = source; + cursor[_range] = range; + cursor[_keyOnly] = keyOnly; + return cursor; + } + + const _direction = Symbol("[[direction]]"); + const _position = Symbol("[[position]]"); + const _gotValue = Symbol("[[gotValue]]"); + const _key = Symbol("[[key]]"); + const _value = Symbol("[[value]]"); + const _range = Symbol("[[range]]"); + const _keyOnly = Symbol("[[keyOnly]]"); + const _effectiveKey = Symbol("[[effectiveKey]]"); + const _effectiveObjectStore = Symbol("[[effectiveObjectStore]]"); + const _objectStorePosition = Symbol("[[objectStorePosition]]"); + const _request = Symbol("[[request]]"); + // Ref: https://w3c.github.io/IndexedDB/#idbcursor + class IDBCursor { + constructor() { + webidl.illegalConstructor(); + } + + /** @type {IDBTransaction} */ + [_transaction]; + + [_position]; + [_gotValue]; + [_value]; + [_range]; + [_keyOnly]; + [_objectStorePosition]; + get [_effectiveObjectStore]() { + if (this[_source] instanceof IDBObjectStore) { + return this[_position]; + } else if (this[_source] instanceof IDBIndex) { + return this[_objectStorePosition]; + } + } + get [_effectiveKey]() { + if (this[_source] instanceof IDBObjectStore) { + return this[_position]; + } else if (this[_source] instanceof IDBIndex) { + return this[_objectStorePosition]; + } + } + + [_source]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-source + get source() { + webidl.assertBranded(this, IDBCursorPrototype); + return this[_source]; + } + + /** @type {IDBCursorDirection} */ + [_direction]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-direction + get direction() { + webidl.assertBranded(this, IDBCursorPrototype); + return this[_direction]; + } + + [_key]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-key + get key() { + webidl.assertBranded(this, IDBCursorPrototype); + return keyToValue(this[_key]); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-primarykey + get primaryKey() { + webidl.assertBranded(this, IDBCursorPrototype); + return keyToValue(this[_effectiveKey]); + } + + [_request]; + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-request + get request() { + webidl.assertBranded(this, IDBCursorPrototype); + return this[_request]; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-advance + advance(count) { + webidl.assertBranded(this, IDBCursorPrototype); + const prefix = "Failed to execute 'advance' on 'IDBCursor'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + count = webidl.converters["unsigned long"](count, { + prefix, + context: "Argument 1", + enforceRange: true, + }); + if (count === 0) { + throw new TypeError("Count cannot be 0"); + } + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + assertCursor(this); + if (!this[_gotValue]) { + throw new DOMException("", "InvalidStateError"); + } + this[_gotValue] = false; + this[_request][_processedDeferred] = new Deferred(); + this[_request][_done] = false; + + return asynchronouslyExecuteRequest( + this, + () => iterateCursor(this, count), + this[_request], + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-continue + continue(key) { + webidl.assertBranded(this, IDBCursorPrototype); + const prefix = "Failed to execute 'key' on 'IDBCursor'"; + key = webidl.converters.any(key, { + prefix, + context: "Argument 1", + }); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + assertCursor(this); + if (key !== undefined) { + key = valueToKey(key); + if (key === null) { + throw new DOMException("", "DataError"); + } + if ( + (compareTwoKeys(key, this[_position]) !== 1) && + (this[_direction] === "next" || this[_direction] === "nextunique") + ) { + throw new DOMException("", "DataError"); + } + if ( + (compareTwoKeys(key, this[_position]) !== -1) && + (this[_direction] === "prev" || this[_direction] === "prevunique") + ) { + throw new DOMException("", "DataError"); + } + } + this[_gotValue] = false; + this[_request][_processedDeferred] = new Deferred(); + this[_request][_done] = false; + + return asynchronouslyExecuteRequest( + this, + () => iterateCursor(this, key), + this[_request], + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-continueprimarykey + continuePrimaryKey(key, primaryKey) { + webidl.assertBranded(this, IDBCursorPrototype); + const prefix = "Failed to execute 'continuePrimaryKey' on 'IDBCursor'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + key = webidl.converters.any(key, { + prefix, + context: "Argument 1", + }); + primaryKey = webidl.converters.any(primaryKey, { + prefix, + context: "Argument 2", + }); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + assertCursor(this); + if (!(this[_source] instanceof IDBIndex)) { + throw new DOMException("", "InvalidAccessError"); + } + if (this[_direction] !== "next" && this[_direction] !== "prev") { + throw new DOMException("", "InvalidAccessError"); + } + if (!this[_gotValue]) { + throw new DOMException("", "InvalidAccessError"); + } + key = valueToKey(key); + if (key === null) { + throw new DOMException("", "DataError"); + } + primaryKey = valueToKey(primaryKey); + if (primaryKey === null) { + throw new DOMException("", "DataError"); + } + if ( + compareTwoKeys(key, this[_direction]) === -1 && + this[_direction] === "next" + ) { + throw new DOMException("", "DataError"); + } + if ( + compareTwoKeys(key, this[_direction]) === 1 && + this[_direction] === "prev" + ) { + throw new DOMException("", "DataError"); + } + if ( + compareTwoKeys(key, this[_direction]) === 0 && + compareTwoKeys(primaryKey, this[_objectStorePosition]) !== 1 && + this[_direction] === "next" + ) { + throw new DOMException("", "DataError"); + } + if ( + compareTwoKeys(key, this[_direction]) === 0 && + compareTwoKeys(primaryKey, this[_objectStorePosition]) !== -1 && + this[_direction] === "prev" + ) { + throw new DOMException("", "DataError"); + } + this[_gotValue] = false; + this[_request][_processedDeferred] = new Deferred(); + this[_request][_done] = false; + + return asynchronouslyExecuteRequest( + this, + () => iterateCursor(this, key, primaryKey), + this[_request], + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-update + update(value) { + webidl.assertBranded(this, IDBCursorPrototype); + const prefix = "Failed to execute 'update' on 'IDBCursor'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + value = webidl.converters.any(value, { + prefix, + context: "Argument 1", + }); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + if (this[_transaction][_mode] === "readonly") { + throw new DOMException("", "ReadOnlyError"); + } + assertCursor(this); + if (!this[_gotValue]) { + throw new DOMException("", "InvalidStateError"); + } + if (this[_keyOnly]) { + throw new DOMException("", "InvalidStateError"); + } + const cloned = clone(value); // TODO: during transaction?: open issue or mail + if (this[_effectiveObjectStore][_store].keyPath !== null) { + const kpk = extractKeyFromValueUsingKeyPath( + cloned, + this[_effectiveObjectStore][_store].keyPath, + ); + if (kpk === null || kpk === _failure || kpk !== this[_effectiveKey]) { + throw new DOMException("", "DataError"); + } + } + + return asynchronouslyExecuteRequest( + this, + () => + storeRecordIntoObjectStore( + this[_effectiveObjectStore], + cloned, + this[_effectiveKey], + false, + ), + ); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-delete + delete() { + webidl.assertBranded(this, IDBCursorPrototype); + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + if (this[_transaction][_mode] === "readonly") { + throw new DOMException("", "ReadOnlyError"); + } + assertCursor(this); + if (!this[_gotValue]) { + throw new DOMException("", "InvalidStateError"); + } + if (this[_keyOnly]) { + throw new DOMException("", "InvalidStateError"); + } + + return asynchronouslyExecuteRequest( + this, + () => + deleteRecordsFromObjectStore( + this[_effectiveObjectStore], + this[_effectiveKey], + ), + ); + } + } + webidl.configurePrototype(IDBCursor); + const IDBCursorPrototype = IDBCursor.prototype; + + const _requestList = Symbol("[[requestList]]"); + const _state = Symbol("[[state]]"); + const _mode = Symbol("[[mode]]"); + const _durabilityHint = Symbol("[[durabilityHint]]"); + const _rid = Symbol("[[rid]]"); + const _connection = Symbol("[[connection]]"); + const _scope = Symbol("[[scope]]"); + // Ref: https://w3c.github.io/IndexedDB/#idbtransaction + class IDBTransaction extends EventTarget { + [_rid]; + + [_requestList] = []; + /** @type {TransactionState} */ + [_state] = "active"; + [_mode]; + [_durabilityHint]; + [_error]; + [_connection]; + [_scope]; + + constructor() { + super(); + webidl.illegalConstructor(); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstorenames + get objectStoreNames() { + webidl.assertBranded(this, IDBTransactionPrototype); + // TODO: from _db and cache + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-mode + get mode() { + webidl.assertBranded(this, IDBTransactionPrototype); + return this[_mode]; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-durability + get durability() { + webidl.assertBranded(this, IDBTransactionPrototype); + return this[_durabilityHint]; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-db + get db() { + webidl.assertBranded(this, IDBTransactionPrototype); + return this[_connection]; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-error + get error() { + webidl.assertBranded(this, IDBTransactionPrototype); + return this[_error]; + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore + objectStore(name) { + webidl.assertBranded(this, IDBTransactionPrototype); + const prefix = "Failed to execute 'objectStore' on 'IDBTransaction'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + name = webidl.converters.DOMString(name, { + prefix, + context: "Argument 1", + }); + if (this[_state] === "finished") { + throw new DOMException("", "InvalidStateError"); + } + // TODO: 2., 3.: cache + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-commit + commit() { + webidl.assertBranded(this, IDBTransactionPrototype); + if (this[_state] !== "active") { + throw new DOMException("", "InvalidStateError"); + } + return commitTransaction(this); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-abort + abort() { + webidl.assertBranded(this, IDBTransactionPrototype); + if (this[_state] === "committing" || this[_state] === "finished") { + throw new DOMException("", "InvalidStateError"); + } + this[_state] = "inactive"; + abortTransaction(this, null); + } + } + defineEventHandler(IDBTransaction.prototype, "abort"); + defineEventHandler(IDBTransaction.prototype, "complete"); + defineEventHandler(IDBTransaction.prototype, "error"); + + webidl.configurePrototype(IDBTransaction); + const IDBTransactionPrototype = IDBTransaction.prototype; + + window.__bootstrap.indexedDb = { + indexeddb: webidl.createBranded(IDBFactory), + IDBRequest, + IDBOpenDBRequest, + IDBFactory, + IDBDatabase, + IDBObjectStore, + IDBIndex, + IDBKeyRange, + IDBCursor, + IDBTransaction, + }; +})(this); diff --git a/ext/webstorage/02_indexeddb_types.d.ts b/ext/webstorage/02_indexeddb_types.d.ts new file mode 100644 index 00000000000000..cf096c2c7b2ddb --- /dev/null +++ b/ext/webstorage/02_indexeddb_types.d.ts @@ -0,0 +1,17 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +// ** Internal Interfaces ** + +interface Key { + type: "number" | "date" | "string" | "binary" | "array"; + // deno-lint-ignore no-explicit-any + value: any; +} + +type TransactionState = "active" | "inactive" | "committing" | "finished"; + +interface KeyGenerator { + current: number; + generateKey(): number; + possiblyUpdate(key: Key): void; +} diff --git a/ext/webstorage/Cargo.toml b/ext/webstorage/Cargo.toml index 9c0d0033a34669..24233e887c654a 100644 --- a/ext/webstorage/Cargo.toml +++ b/ext/webstorage/Cargo.toml @@ -14,7 +14,9 @@ description = "Implementation of WebStorage API for Deno" path = "lib.rs" [dependencies] +async-trait = "0.1.52" deno_core = { version = "0.124.0", path = "../../core" } deno_web = { version = "0.73.0", path = "../web" } -rusqlite = { version = "0.25.3", features = ["unlock_notify", "bundled"] } +fallible-iterator = "0.2.0" +rusqlite = { version = "0.25.3", features = ["unlock_notify", "bundled", "serde_json"] } serde = { version = "1.0.129", features = ["derive"] } diff --git a/ext/webstorage/indexeddb/mod.rs b/ext/webstorage/indexeddb/mod.rs new file mode 100644 index 00000000000000..2a0dde8a578725 --- /dev/null +++ b/ext/webstorage/indexeddb/mod.rs @@ -0,0 +1,959 @@ +use std::borrow::{Cow}; +use std::cell::RefCell; +use super::DomExceptionNotSupportedError; +use super::OriginStorageDir; +use crate::{DomExceptionConstraintError, DomExceptionInvalidStateError}; +use deno_core::error::AnyError; +use deno_core::{op, Resource}; +use deno_core::serde_json; +use deno_core::OpState; +use deno_core::ResourceId; +use deno_core::ZeroCopyBuf; +use fallible_iterator::FallibleIterator; +use rusqlite::params; +use rusqlite::types::FromSqlResult; +use rusqlite::types::ToSqlOutput; +use rusqlite::types::ValueRef; +use rusqlite::Connection; +use rusqlite::OptionalExtension; +use serde::Deserialize; +use serde::Serialize; +use std::cmp::Ordering; +use std::rc::Rc; + +#[derive(Clone)] +struct Database { + name: String, + version: u64, +} + +pub struct IndexedDb(Rc>, Option>); + +impl Resource for IndexedDb { + fn name(&self) -> Cow { + "indexedDb".into() + } +} + +#[op] +pub fn op_indexeddb_open(state: &mut OpState) -> Result<(), AnyError> { + let path = state.try_borrow::().ok_or_else(|| { + DomExceptionNotSupportedError::new( + "IndexedDB is not supported in this context.", + ) + })?; + std::fs::create_dir_all(&path.0)?; + let conn = Connection::open(path.0.join("indexeddb"))?; + let initial_pragmas = " + -- enable write-ahead-logging mode + PRAGMA recursive_triggers = ON; + PRAGMA secure_delete = OFF; + PRAGMA foreign_keys = ON; + "; + conn.execute_batch(initial_pragmas)?; + + let create_statements = r#" + CREATE TABLE IF NOT EXISTS database ( + name TEXT PRIMARY KEY, + version INTEGER NOT NULL DEFAULT 0 + ) WITHOUT ROWID; + + CREATE TABLE IF NOT EXISTS object_store ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + key_path TEXT, + unique_index INTEGER NOT NULL, + database_name TEXT NOT NULL, + FOREIGN KEY (database_name) + REFERENCES database(name) + ); + + CREATE TABLE IF NOT EXISTS record ( + object_store_id INTEGER NOT NULL, + key BLOB NOT NULL, + index_data_values BLOB DEFAULT NULL, + value BLOB NOT NULL, + PRIMARY KEY (object_store_id, key), + FOREIGN KEY (object_store_id) + REFERENCES object_store(id) + ) WITHOUT ROWID; + + CREATE TABLE index ( + id INTEGER PRIMARY KEY, + object_store_id INTEGER NOT NULL, + name TEXT NOT NULL, + key_path TEXT NOT NULL, + unique INTEGER NOT NULL, + multientry INTEGER NOT NULL, + FOREIGN KEY (object_store_id) + REFERENCES object_store(id) + ); + + CREATE TABLE IF NOT EXISTS index_data ( + index_id INTEGER NOT NULL, + value BLOB NOT NULL, + record_key BLOB NOT NULL, + object_store_id INTEGER NOT NULL, + PRIMARY KEY (index_id, value, record_key), + FOREIGN KEY (index_id) + REFERENCES index(id), + FOREIGN KEY (object_store_id, record_key) + REFERENCES record(object_store_id, key) + ) WITHOUT ROWID; + + CREATE TABLE IF NOT EXISTS unique_index_data ( + index_id INTEGER NOT NULL, + value BLOB NOT NULL, + record_key BLOB NOT NULL, + object_store_id INTEGER NOT NULL, + PRIMARY KEY (index_id, value), + FOREIGN KEY (index_id) + REFERENCES index(id), + FOREIGN KEY (object_store_id, record_key) + REFERENCES record(object_store_id, key) + ) WITHOUT ROWID; + "#; + conn.execute_batch(create_statements)?; + + conn.set_prepared_statement_cache_capacity(128); + state.resource_table.add(IndexedDb(Rc::new(RefCell::new(conn)), None)); + + Ok(()) +} + +#[op] +pub fn op_indexeddb_transaction_create( + state: &mut OpState, +) -> Result { + let idbmanager = state.borrow::(); + let mut conn = idbmanager.0.borrow_mut(); + let transaction = conn.transaction()?; + let rid = state.resource_table.add(IndexedDb(idbmanager.0.clone(), transaction)); + Ok(rid) +} + +#[op] +pub fn op_indexeddb_transaction_commit( + state: &mut OpState, + rid: ResourceId, +) -> Result<(), AnyError> { + let idb = Rc::try_unwrap(state.resource_table.take::(rid)?).unwrap(); + idb.1.commit()?; + Ok(()) +} + +#[op] +pub fn op_indexeddb_transaction_abort( + state: &mut OpState, + rid: ResourceId, +) -> Result<(), AnyError> { + let idb = Rc::try_unwrap(state.resource_table.take::(rid)?).unwrap(); + idb.1.rollback()?; + Ok(()) +} + +// Ref: https://w3c.github.io/IndexedDB/#open-a-database +#[op] +pub fn op_indexeddb_open_database( + state: &mut OpState, + name: String, + version: Option, +) -> Result<(u64, u64), AnyError> { + let idbmanager = state.borrow::(); + let conn = &idbmanager.0; + let mut stmt = + conn.prepare_cached("SELECT * FROM database WHERE name = ?")?; + let db = stmt + .query_row(params![name], |row| { + Ok(Database { + name: row.get(0)?, + version: row.get(1)?, + }) + }) + .optional()?; + let version = version + .or_else(|| db.clone().map(|db| db.version)) + .unwrap_or(1); + + let db = if let Some(db) = db { + db + } else { + let mut stmt = + conn.prepare_cached("INSERT INTO database (name) VALUES (?)")?; + stmt.execute(params![name])?; // TODO: 6. DOMException + Database { name, version: 0 } + }; + + Ok((version, db.version)) +} + +// Ref: https://w3c.github.io/IndexedDB/#dom-idbfactory-databases +#[op] +pub fn op_indexeddb_list_databases( + state: &mut OpState, +) -> Result, AnyError> { + let idbmanager = &state.borrow::().0; + let mut stmt = idbmanager.prepare_cached("SELECT name FROM database")?; + let names = stmt + .query(params![])? + .map(|row| row.get(0)) + .collect::>()?; + Ok(names) +} + +// Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore +#[op] +pub fn op_indexeddb_database_create_object_store( + state: &mut OpState, + database_name: String, + name: String, + key_path: serde_json::Value, +) -> Result<(), AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT * FROM object_store WHERE name = ? AND database_name = ?", + )?; + if stmt.exists(params![name, database_name])? { + return Err( + DomExceptionConstraintError::new(&format!( + "ObjectStore with name '{name}' already exists" + )) + .into(), + ); + } + + // TODO: 8. + + let mut stmt = conn.prepare_cached( + "INSERT INTO object_store (name, key_path, unique_index, database_name) VALUES (?, ?, ?, ?)", + )?; + stmt.execute(params![ + name, + key_path, + // TODO: unique_index + database_name, + ])?; + + Ok(()) +} + +// Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-deleteobjectstore +#[op] +pub fn op_indexeddb_database_delete_object_store( + state: &mut OpState, + database_name: String, + name: String, +) -> Result<(), AnyError> { + let conn = &state.borrow::().0; + + let mut stmt: rusqlite::CachedStatement = conn.prepare_cached( + "DELETE FROM object_store WHERE name = ? AND database_name = ?", + )?; + stmt.execute(params![name, database_name])?; + + // TODO: delete indexes & records. maybe use ON DELETE CASCADE? + + Ok(()) +} + +#[op] +pub fn op_indexeddb_object_store_exists( + state: &mut OpState, + database_name: String, + name: String, +) -> Result<(), AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT * FROM object_store WHERE name = ? AND database_name = ?", + )?; + if !stmt.exists(params![name, database_name])? { + return Err( + DomExceptionInvalidStateError::new(&format!( + "ObjectStore with name '{name}' does not exists" + )) + .into(), + ); + } + + Ok(()) +} + +// Ref: https://w3c.github.io/IndexedDB/#ref-for-dom-idbobjectstore-name%E2%91%A2 +#[op] +pub fn op_indexeddb_object_store_rename( + state: &mut OpState, + database_name: String, + prev_name: String, + new_name: String, +) -> Result<(), AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT * FROM object_store WHERE name = ? AND database_name = ?", + )?; + if stmt.exists(params![new_name, database_name])? { + return Err( + DomExceptionConstraintError::new(&format!( + "ObjectStore with name '{new_name}' already exists" + )) + .into(), + ); + } + + let mut stmt = conn.prepare_cached( + "UPDATE object_store SET name = ? WHERE name = ? AND database_name = ?", + )?; + stmt.execute(params![new_name, prev_name, database_name])?; + + Ok(()) +} + +// Ref: https://w3c.github.io/IndexedDB/#key-construct +#[derive(Deserialize, Serialize, Clone)] +#[serde(tag = "type", content = "value")] +pub enum Key { + Number(u64), + Date(u64), + String(String), + Binary(ZeroCopyBuf), + Array(Box>), +} + +impl Key { + // Ref: https://w3c.github.io/IndexedDB/#compare-two-keys + fn cmp(&self, other: &Self) -> Ordering { + if std::mem::discriminant(self) != std::mem::discriminant(other) { + if let Key::Array(_) = self { + Ordering::Greater + } else if let Key::Array(_) = other { + Ordering::Less + } else if let Key::Binary(_) = self { + Ordering::Greater + } else if let Key::Binary(_) = other { + Ordering::Less + } else if let Key::String(_) = self { + Ordering::Greater + } else if let Key::String(_) = other { + Ordering::Less + } else if let Key::Number(_) = self { + Ordering::Greater + } else if let Key::Number(_) = other { + Ordering::Less + } else if let Key::Date(_) = self { + Ordering::Greater + } else if let Key::Date(_) = other { + Ordering::Less + } else { + unreachable!() + } + } else { + match (self, other) { + (Key::Number(va), Key::Number(vb)) | (Key::Date(va), Key::Date(vb)) => { + va.cmp(vb) + } + (Key::String(va), Key::String(vb)) => va.cmp(vb), + (Key::Binary(va), Key::Binary(vb)) => va.cmp(vb), + (Key::Array(va), Key::Array(vb)) => { + for x in va.iter().zip(vb.iter()) { + match x.0.cmp(x.1) { + Ordering::Greater => {} + res => return res, + } + } + va.len().cmp(&vb.len()) + } + _ => unreachable!(), + } + } + } +} + +impl rusqlite::types::ToSql for Key { + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Blob( + serde_json::to_vec(self) + .map_err(|e| rusqlite::Error::ToSqlConversionFailure(e.into()))?, + ), + )) + } +} + +impl rusqlite::types::FromSql for Key { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + value.as_blob().and_then(|blob| { + Ok( + serde_json::from_slice(blob) + .map_err(|e| rusqlite::types::FromSqlError::Other(e.into()))?, + ) + }) + } +} + +// Ref: https://w3c.github.io/IndexedDB/#range-construct +#[derive(Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Range { + lower: Option, + upper: Option, + lower_open: bool, + upper_open: bool, +} + +impl Range { + // Ref: https://w3c.github.io/IndexedDB/#in + fn contains(&self, key: &Key) -> bool { + let lower = match &self.lower { + Some(lower_key) => match lower_key.cmp(key) { + Ordering::Less => true, + Ordering::Equal if !self.lower_open => true, + _ => false, + }, + None => true, + }; + let upper = match &self.upper { + Some(upper_key) => match upper_key.cmp(key) { + Ordering::Greater => true, + Ordering::Equal if !self.upper_open => true, + _ => false, + }, + None => true, + }; + lower && upper + } +} + +#[derive(Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct Index { + id: u64, + object_store_id: u64, + database_name: String, + name: String, + key_path: serde_json::Value, + unique: bool, + multi_entry: bool, +} + +// Ref: https://w3c.github.io/IndexedDB/#delete-records-from-an-object-store +#[op] +pub fn op_indexeddb_object_store_add_or_put_records( + state: &mut OpState, + database_name: String, + store_name: String, + value: ZeroCopyBuf, + key: Key, + no_overwrite: bool, +) -> Result, AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let query = if no_overwrite { + "INSERT INTO record (object_store_id, key, value) VALUES (?, ?, ?)" + } else { + "INSERT OR REPLACE INTO record (object_store_id, key, value) VALUES (?, ?, ?)" + }; + + let mut stmt = conn.prepare_cached(query)?; + stmt.execute(params![object_store_id, key, value.to_vec()])?; + // TODO: keys are to be sorted (4.) + + let mut stmt = + conn.prepare_cached("SELECT * FROM index WHERE object_store_id = ?")?; + let indexes = stmt + .query_map(params![object_store_id], |row| { + Ok(Index { + id: row.get(0)?, + object_store_id: row.get(1)?, + database_name: row.get(2)?, + name: row.get(3)?, + key_path: row.get(4)?, + unique: row.get(5)?, + multi_entry: row.get(6)?, + }) + })? + .collect::>()?; + + Ok(indexes) +} + +// For: https://w3c.github.io/IndexedDB/#store-a-record-into-an-object-store +#[op] +pub fn op_indexeddb_object_store_add_or_put_records_handle_index( + state: &mut OpState, + index: Index, + index_key: Key, +) -> Result<(), AnyError> { + let conn = &state.borrow::().0; + + if !index.multi_entry + || matches!( + index_key, + Key::String(_) | Key::Date(_) | Key::Binary(_) | Key::Number(_) + ) + { + let mut stmt = conn.prepare_cached( + "SELECT record_Key FROM unique_index_data WHERE index_id = ?", + )?; + let key = stmt + .query_map(params![index.id], |row| row.get::(0))? + .find_map(|key| { + key + .map(|key| { + if matches!(key.cmp(&index_key), Ordering::Equal) { + Some(key) + } else { + None + } + }) + .transpose() + }); + if key.is_some() { + return Err(DomExceptionConstraintError::new("").into()); // TODO + } + } + if index.multi_entry { + if let Key::Array(keys) = &index_key { + let mut stmt = conn.prepare_cached( + "SELECT record_Key FROM unique_index_data WHERE index_id = ?", + )?; + let key = stmt + .query_map(params![index.id], |row| row.get::(0))? + .find_map(|key| { + key + .map(|key| { + if keys + .iter() + .any(|subkey| matches!(key.cmp(subkey), Ordering::Equal)) + { + Some(key) + } else { + None + } + }) + .transpose() + }); + if key.is_some() { + return Err(DomExceptionConstraintError::new("").into()); // TODO + } + } + } + if !index.multi_entry + || matches!( + index_key, + Key::String(_) | Key::Date(_) | Key::Binary(_) | Key::Number(_) + ) + { + // TODO: 5. + } + if index.multi_entry { + if let Key::Array(keys) = index_key { + for key in *keys { + // TODO: 6. + } + } + } + + Ok(()) +} + +// Ref: https://w3c.github.io/IndexedDB/#delete-records-from-an-object-store +#[op] +pub fn op_indexeddb_object_store_delete_records( + state: &mut OpState, + database_name: String, + store_name: String, + range: Range, +) -> Result<(), AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = conn + .prepare_cached("SELECT id, key FROM record WHERE object_store_id = ?")?; + let mut delete_stmt = + conn.prepare_cached("DELETE FROM record WHERE id = ?")?; + for row in stmt.query_map(params![object_store_id], |row| { + Ok((row.get::(0)?, row.get::(1)?)) + })? { + let (id, key) = row?; + if range.contains(&key) { + delete_stmt.execute(params![id])?; + } + } + + let mut stmt = conn.prepare_cached( + "SELECT index_id, value FROM index_data WHERE object_store_id = ?", + )?; + let mut delete_stmt = + conn.prepare_cached("DELETE FROM index_data WHERE index_id = ?")?; + for row in stmt.query_map(params![object_store_id], |row| { + Ok((row.get::(0)?, row.get::(1)?)) + })? { + let (id, key) = row?; + if range.contains(&key) { + delete_stmt.execute(params![id])?; + } + } + + Ok(()) +} + +// Ref: https://w3c.github.io/IndexedDB/#clear-an-object-store +#[op] +pub fn op_indexeddb_object_store_clear( + state: &mut OpState, + database_name: String, + store_name: String, +) -> Result<(), AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = + conn.prepare_cached("DELETE FROM record WHERE object_store_id = ?")?; + stmt.execute(params![object_store_id])?; + + let mut stmt = + conn.prepare_cached("DELETE FROM index_data WHERE object_store_id = ?")?; + stmt.execute(params![object_store_id])?; + + Ok(()) +} + +// Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-object-store +#[op] +pub fn op_indexeddb_object_store_retrieve_value( + state: &mut OpState, + database_name: String, + store_name: String, + range: Range, +) -> Result, AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = conn.prepare_cached( + "SELECT key, value FROM record WHERE object_store_id = ?", + )?; + for row in stmt.query_map(params![object_store_id], |row| { + Ok((row.get::(0)?, row.get::>(1)?)) + })? { + let (key, value) = row?; + if range.contains(&key) { + return Ok(Some(value.into())); + } + } + + Ok(None) +} + +// Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-values-from-an-object-store +#[op] +pub fn op_indexeddb_object_store_retrieve_multiple_values( + state: &mut OpState, + database_name: String, + store_name: String, + range: Range, + count: Option, +) -> Result, AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = conn.prepare_cached( + "SELECT key, value FROM record WHERE object_store_id = ?", + )?; + let res = stmt + .query_map(params![object_store_id], |row| { + Ok((row.get::(0)?, row.get::>(1)?)) + })? + .filter_map(|row| { + row + .map(|(key, val)| { + if range.contains(&key) { + Some(val.into()) + } else { + None + } + }) + .transpose() + }); + + Ok(if let Some(count) = count { + res + .take(count as usize) + .collect::>()? + } else { + res.collect::>()? + }) +} + +// Ref: https://w3c.github.io/IndexedDB/#retrieve-a-key-from-an-object-store +#[op] +pub fn op_indexeddb_object_store_retrieve_key( + state: &mut OpState, + database_name: String, + store_name: String, + range: Range, +) -> Result, AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = + conn.prepare_cached("SELECT key FROM record WHERE object_store_id = ?")?; + for row in + stmt.query_map(params![object_store_id], |row| row.get::(0))? + { + let key = row?; + if range.contains(&key) { + return Ok(Some(key)); + } + } + + Ok(None) +} + +// Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-keys-from-an-object-store +#[op] +pub fn op_indexeddb_object_store_retrieve_multiple_keys( + state: &mut OpState, + database_name: String, + store_name: String, + range: Range, + count: Option, +) -> Result, AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = + conn.prepare_cached("SELECT key FROM record WHERE object_store_id = ?")?; + let res = stmt + .query_map(params![object_store_id], |row| row.get::(0))? + .filter_map(|row| { + row + .map(|key| { + if range.contains(&key) { + Some(key) + } else { + None + } + }) + .transpose() + }); + + Ok(if let Some(count) = count { + res + .take(count as usize) + .collect::>()? + } else { + res.collect::>()? + }) +} + +// Ref: https://w3c.github.io/IndexedDB/#count-the-records-in-a-range +#[op] +pub fn op_indexeddb_object_store_count_records( + state: &mut OpState, + database_name: String, + store_name: String, + range: Range, +) -> Result { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = conn.prepare_cached( + "SELECT key, value FROM record WHERE object_store_id = ?", + )?; + let res = stmt + .query_map(params![object_store_id], |row| { + Ok((row.get::(0)?, row.get::>(1)?)) + })? + .filter_map(|row| { + row + .map(|(key, val)| { + if range.contains(&key) { + Some(()) + } else { + None + } + }) + .transpose() + }); + + Ok(res.count() as u64) +} + +#[op] +pub fn op_indexeddb_index_exists( + state: &mut OpState, + database_name: String, + store_name: String, + index_name: String, +) -> Result<(), AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = conn.prepare_cached( + "SELECT * FROM index WHERE name = ? AND object_store_id = ?", + )?; + if !stmt.exists(params![index_name, object_store_id])? { + return Err( + DomExceptionInvalidStateError::new(&format!( + "Index with name '{index_name}' does not exists" + )) + .into(), + ); + } + + Ok(()) +} + +// Ref: https://w3c.github.io/IndexedDB/#count-the-records-in-a-range +#[op] +pub fn op_indexeddb_index_count_records( + state: &mut OpState, + database_name: String, + store_name: String, + index_name: String, + range: Range, +) -> Result { + todo!() +} + +// Ref: https://w3c.github.io/IndexedDB/#retrieve-a-referenced-value-from-an-index +// Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-index +#[op] +pub fn op_indexeddb_index_retrieve_value( + state: &mut OpState, + database_name: String, + store_name: String, + index_name: String, + range: Range, +) -> Result, AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM index WHERE name = ? AND object_store_id = ?", + )?; + let index_id: u64 = + stmt.query_row(params![index_name, object_store_id], |row| row.get(0))?; + + todo!(); + + Ok(None) +} + +// Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-referenced-values-from-an-index +// Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-index +#[op] +pub fn op_indexeddb_index_retrieve_multiple_values( + state: &mut OpState, + database_name: String, + store_name: String, + index_name: String, + range: Range, + count: Option, +) -> Result, AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM index WHERE name = ? AND object_store_id = ?", + )?; + let index_id: u64 = + stmt.query_row(params![index_name, object_store_id], |row| row.get(0))?; + + todo!(); + + Ok(Default::default()) +} + +#[derive(Serialize, Clone)] +enum Direction { + Next, + Nextunique, + Prev, + Prevunique, +} + +// Ref: https://w3c.github.io/IndexedDB/#iterate-a-cursor +#[op] +pub fn op_indexeddb_object_store_get_records( + state: &mut OpState, + database_name: String, + store_name: String, +) -> Result)>, AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "SELECT id FROM object_store WHERE name = ? AND database_name = ?", + )?; + let object_store_id: u64 = + stmt.query_row(params![store_name, database_name], |row| row.get(0))?; + + let mut stmt = conn.prepare_cached( + "SELECT key, value FROM record WHERE object_store_id = ?", + )?; + let res = stmt + .query_map(params![object_store_id], |row| { + Ok((row.get::(0)?, row.get::>(1)?)) + })? + .collect::>()?; + + Ok(res) +} diff --git a/ext/webstorage/lib.rs b/ext/webstorage/lib.rs index 7cda0176a5b3a4..be79da4142e815 100644 --- a/ext/webstorage/lib.rs +++ b/ext/webstorage/lib.rs @@ -2,37 +2,33 @@ // NOTE to all: use **cached** prepared statements when interfacing with SQLite. +mod indexeddb; +mod webstorage; + use deno_core::error::AnyError; use deno_core::include_js_files; -use deno_core::op; use deno_core::Extension; -use deno_core::OpState; -use rusqlite::params; -use rusqlite::Connection; -use rusqlite::OptionalExtension; -use serde::Deserialize; use std::fmt; use std::path::PathBuf; #[derive(Clone)] struct OriginStorageDir(PathBuf); -const MAX_STORAGE_BYTES: u32 = 10 * 1024 * 1024; - pub fn init(origin_storage_dir: Option) -> Extension { Extension::builder() .js(include_js_files!( prefix "deno:ext/webstorage", "01_webstorage.js", + "02_indexeddb.js", )) .ops(vec![ - op_webstorage_length::decl(), - op_webstorage_key::decl(), - op_webstorage_set::decl(), - op_webstorage_get::decl(), - op_webstorage_remove::decl(), - op_webstorage_clear::decl(), - op_webstorage_iterate_keys::decl(), + webstorage::op_webstorage_length::decl(), + webstorage::op_webstorage_key::decl(), + webstorage::op_webstorage_set::decl(), + webstorage::op_webstorage_get::decl(), + webstorage::op_webstorage_remove::decl(), + webstorage::op_webstorage_clear::decl(), + webstorage::op_webstorage_iterate_keys::decl(), ]) .state(move |state| { if let Some(origin_storage_dir) = &origin_storage_dir { @@ -47,212 +43,110 @@ pub fn get_declaration() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_webstorage.d.ts") } -struct LocalStorage(Connection); -struct SessionStorage(Connection); - -fn get_webstorage( - state: &mut OpState, - persistent: bool, -) -> Result<&Connection, AnyError> { - let conn = if persistent { - if state.try_borrow::().is_none() { - let path = state.try_borrow::().ok_or_else(|| { - DomExceptionNotSupportedError::new( - "LocalStorage is not supported in this context.", - ) - })?; - std::fs::create_dir_all(&path.0)?; - let conn = Connection::open(path.0.join("local_storage"))?; - // Enable write-ahead-logging and tweak some other stuff. - let initial_pragmas = " - -- enable write-ahead-logging mode - PRAGMA journal_mode=WAL; - PRAGMA synchronous=NORMAL; - PRAGMA temp_store=memory; - PRAGMA page_size=4096; - PRAGMA mmap_size=6000000; - PRAGMA optimize; - "; - - conn.execute_batch(initial_pragmas)?; - conn.set_prepared_statement_cache_capacity(128); - { - let mut stmt = conn.prepare_cached( - "CREATE TABLE IF NOT EXISTS data (key VARCHAR UNIQUE, value VARCHAR)", - )?; - stmt.execute(params![])?; - } - state.put(LocalStorage(conn)); - } +#[derive(Debug)] +pub struct DomExceptionNotSupportedError { + pub msg: String, +} - &state.borrow::().0 - } else { - if state.try_borrow::().is_none() { - let conn = Connection::open_in_memory()?; - { - let mut stmt = conn.prepare_cached( - "CREATE TABLE data (key VARCHAR UNIQUE, value VARCHAR)", - )?; - stmt.execute(params![])?; - } - state.put(SessionStorage(conn)); +impl DomExceptionNotSupportedError { + pub fn new(msg: &str) -> Self { + DomExceptionNotSupportedError { + msg: msg.to_string(), } - - &state.borrow::().0 - }; - - Ok(conn) + } } -#[op] -pub fn op_webstorage_length( - state: &mut OpState, - persistent: bool, -) -> Result { - let conn = get_webstorage(state, persistent)?; - - let mut stmt = conn.prepare_cached("SELECT COUNT(*) FROM data")?; - let length: u32 = stmt.query_row(params![], |row| row.get(0))?; - - Ok(length) +impl fmt::Display for DomExceptionNotSupportedError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.pad(&self.msg) + } } -#[op] -pub fn op_webstorage_key( - state: &mut OpState, - index: u32, - persistent: bool, -) -> Result, AnyError> { - let conn = get_webstorage(state, persistent)?; - - let mut stmt = - conn.prepare_cached("SELECT key FROM data LIMIT 1 OFFSET ?")?; - - let key: Option = stmt - .query_row(params![index], |row| row.get(0)) - .optional()?; +impl std::error::Error for DomExceptionNotSupportedError {} - Ok(key) +pub fn get_not_supported_error_class_name( + e: &AnyError, +) -> Option<&'static str> { + e.downcast_ref::() + .map(|_| "DOMExceptionNotSupportedError") } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SetArgs { - key_name: String, - key_value: String, +#[derive(Debug)] +pub struct DomExceptionVersionError { + pub msg: String, } -#[op] -pub fn op_webstorage_set( - state: &mut OpState, - args: SetArgs, - persistent: bool, -) -> Result<(), AnyError> { - let conn = get_webstorage(state, persistent)?; - - let mut stmt = conn - .prepare_cached("SELECT SUM(pgsize) FROM dbstat WHERE name = 'data'")?; - let size: u32 = stmt.query_row(params![], |row| row.get(0))?; - - if size >= MAX_STORAGE_BYTES { - return Err( - deno_web::DomExceptionQuotaExceededError::new( - "Exceeded maximum storage size", - ) - .into(), - ); +impl DomExceptionVersionError { + pub fn new(msg: &str) -> Self { + DomExceptionVersionError { + msg: msg.to_string(), + } } - - let mut stmt = conn - .prepare_cached("INSERT OR REPLACE INTO data (key, value) VALUES (?, ?)")?; - stmt.execute(params![args.key_name, args.key_value])?; - - Ok(()) } -#[op] -pub fn op_webstorage_get( - state: &mut OpState, - key_name: String, - persistent: bool, -) -> Result, AnyError> { - let conn = get_webstorage(state, persistent)?; - - let mut stmt = conn.prepare_cached("SELECT value FROM data WHERE key = ?")?; - let val = stmt - .query_row(params![key_name], |row| row.get(0)) - .optional()?; - - Ok(val) +impl fmt::Display for DomExceptionVersionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.pad(&self.msg) + } } -#[op] -pub fn op_webstorage_remove( - state: &mut OpState, - key_name: String, - persistent: bool, -) -> Result<(), AnyError> { - let conn = get_webstorage(state, persistent)?; - - let mut stmt = conn.prepare_cached("DELETE FROM data WHERE key = ?")?; - stmt.execute(params![key_name])?; +impl std::error::Error for DomExceptionVersionError {} - Ok(()) +pub fn get_version_error_class_name(e: &AnyError) -> Option<&'static str> { + e.downcast_ref::() + .map(|_| "DOMExceptionVersionError") } -#[op] -pub fn op_webstorage_clear( - state: &mut OpState, - persistent: bool, -) -> Result<(), AnyError> { - let conn = get_webstorage(state, persistent)?; - - let mut stmt = conn.prepare_cached("DELETE FROM data")?; - stmt.execute(params![])?; +#[derive(Debug)] +pub struct DomExceptionConstraintError { + pub msg: String, +} - Ok(()) +impl DomExceptionConstraintError { + pub fn new(msg: &str) -> Self { + DomExceptionConstraintError { + msg: msg.to_string(), + } + } } -#[op] -pub fn op_webstorage_iterate_keys( - state: &mut OpState, - persistent: bool, -) -> Result, AnyError> { - let conn = get_webstorage(state, persistent)?; +impl fmt::Display for DomExceptionConstraintError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.pad(&self.msg) + } +} - let mut stmt = conn.prepare_cached("SELECT key FROM data")?; - let keys = stmt - .query_map(params![], |row| row.get::<_, String>(0))? - .map(|r| r.unwrap()) - .collect(); +impl std::error::Error for DomExceptionConstraintError {} - Ok(keys) +pub fn get_constraint_error_class_name(e: &AnyError) -> Option<&'static str> { + e.downcast_ref::() + .map(|_| "DOMExceptionConstraintError") } #[derive(Debug)] -pub struct DomExceptionNotSupportedError { +pub struct DomExceptionInvalidStateError { pub msg: String, } -impl DomExceptionNotSupportedError { +impl DomExceptionInvalidStateError { pub fn new(msg: &str) -> Self { - DomExceptionNotSupportedError { + DomExceptionInvalidStateError { msg: msg.to_string(), } } } -impl fmt::Display for DomExceptionNotSupportedError { +impl fmt::Display for DomExceptionInvalidStateError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.pad(&self.msg) } } -impl std::error::Error for DomExceptionNotSupportedError {} +impl std::error::Error for DomExceptionInvalidStateError {} -pub fn get_not_supported_error_class_name( +pub fn get_invalid_state_error_class_name( e: &AnyError, ) -> Option<&'static str> { - e.downcast_ref::() - .map(|_| "DOMExceptionNotSupportedError") + e.downcast_ref::() + .map(|_| "DOMExceptionInvalidStateError") } diff --git a/ext/webstorage/webstorage.rs b/ext/webstorage/webstorage.rs new file mode 100644 index 00000000000000..ed6377041731b0 --- /dev/null +++ b/ext/webstorage/webstorage.rs @@ -0,0 +1,196 @@ +use super::DomExceptionNotSupportedError; +use super::OriginStorageDir; +use deno_core::error::AnyError; +use deno_core::op; +use deno_core::OpState; +use rusqlite::params; +use rusqlite::Connection; +use rusqlite::OptionalExtension; +use serde::Deserialize; + +struct LocalStorage(Connection); +struct SessionStorage(Connection); + +const MAX_STORAGE_BYTES: u32 = 10 * 1024 * 1024; + +fn get_webstorage( + state: &mut OpState, + persistent: bool, +) -> Result<&Connection, AnyError> { + let conn = if persistent { + if state.try_borrow::().is_none() { + let path = state.try_borrow::().ok_or_else(|| { + DomExceptionNotSupportedError::new( + "LocalStorage is not supported in this context.", + ) + })?; + std::fs::create_dir_all(&path.0)?; + let conn = Connection::open(path.0.join("local_storage"))?; + // Enable write-ahead-logging and tweak some other stuff. + let initial_pragmas = " + -- enable write-ahead-logging mode + PRAGMA journal_mode=WAL; + PRAGMA synchronous=NORMAL; + PRAGMA temp_store=memory; + PRAGMA page_size=4096; + PRAGMA mmap_size=6000000; + PRAGMA optimize; + "; + + conn.execute_batch(initial_pragmas)?; + conn.set_prepared_statement_cache_capacity(128); + { + let mut stmt = conn.prepare_cached( + "CREATE TABLE IF NOT EXISTS data (key VARCHAR UNIQUE, value VARCHAR)", + )?; + stmt.execute(params![])?; + } + state.put(LocalStorage(conn)); + } + + &state.borrow::().0 + } else { + if state.try_borrow::().is_none() { + let conn = Connection::open_in_memory()?; + { + let mut stmt = conn.prepare_cached( + "CREATE TABLE data (key VARCHAR UNIQUE, value VARCHAR)", + )?; + stmt.execute(params![])?; + } + state.put(SessionStorage(conn)); + } + + &state.borrow::().0 + }; + + Ok(conn) +} + +#[op] +pub fn op_webstorage_length( + state: &mut OpState, + persistent: bool, + _: (), +) -> Result { + let conn = get_webstorage(state, persistent)?; + + let mut stmt = conn.prepare_cached("SELECT COUNT(*) FROM data")?; + let length: u32 = stmt.query_row(params![], |row| row.get(0))?; + + Ok(length) +} + +#[op] +pub fn op_webstorage_key( + state: &mut OpState, + index: u32, + persistent: bool, +) -> Result, AnyError> { + let conn = get_webstorage(state, persistent)?; + + let mut stmt = + conn.prepare_cached("SELECT key FROM data LIMIT 1 OFFSET ?")?; + + let key: Option = stmt + .query_row(params![index], |row| row.get(0)) + .optional()?; + + Ok(key) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SetArgs { + key_name: String, + key_value: String, +} + +#[op] +pub fn op_webstorage_set( + state: &mut OpState, + args: SetArgs, + persistent: bool, +) -> Result<(), AnyError> { + let conn = get_webstorage(state, persistent)?; + + let mut stmt = conn + .prepare_cached("SELECT SUM(pgsize) FROM dbstat WHERE name = 'data'")?; + let size: u32 = stmt.query_row(params![], |row| row.get(0))?; + + if size >= MAX_STORAGE_BYTES { + return Err( + deno_web::DomExceptionQuotaExceededError::new( + "Exceeded maximum storage size", + ) + .into(), + ); + } + + let mut stmt = conn + .prepare_cached("INSERT OR REPLACE INTO data (key, value) VALUES (?, ?)")?; + stmt.execute(params![args.key_name, args.key_value])?; + + Ok(()) +} + +#[op] +pub fn op_webstorage_get( + state: &mut OpState, + key_name: String, + persistent: bool, +) -> Result, AnyError> { + let conn = get_webstorage(state, persistent)?; + + let mut stmt = conn.prepare_cached("SELECT value FROM data WHERE key = ?")?; + let val = stmt + .query_row(params![key_name], |row| row.get(0)) + .optional()?; + + Ok(val) +} + +#[op] +pub fn op_webstorage_remove( + state: &mut OpState, + key_name: String, + persistent: bool, +) -> Result<(), AnyError> { + let conn = get_webstorage(state, persistent)?; + + let mut stmt = conn.prepare_cached("DELETE FROM data WHERE key = ?")?; + stmt.execute(params![key_name])?; + + Ok(()) +} + +#[op] +pub fn op_webstorage_clear( + state: &mut OpState, + persistent: bool, + _: (), +) -> Result<(), AnyError> { + let conn = get_webstorage(state, persistent)?; + + let mut stmt = conn.prepare_cached("DELETE FROM data")?; + stmt.execute(params![])?; + + Ok(()) +} + +#[op] +pub fn op_webstorage_iterate_keys( + state: &mut OpState, + persistent: bool, + _: (), +) -> Result, AnyError> { + let conn = get_webstorage(state, persistent)?; + + let mut stmt = conn.prepare_cached("SELECT key FROM data")?; + let keys = stmt + .query_map(params![], |row| row.get::<_, String>(0))? + .map(|r| r.unwrap()) + .collect(); + + Ok(keys) +} diff --git a/runtime/errors.rs b/runtime/errors.rs index 0f6df5828c6980..682bb06f2aa808 100644 --- a/runtime/errors.rs +++ b/runtime/errors.rs @@ -157,6 +157,9 @@ pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> { .or_else(|| deno_webgpu::error::get_error_class_name(e)) .or_else(|| deno_web::get_error_class_name(e)) .or_else(|| deno_webstorage::get_not_supported_error_class_name(e)) + .or_else(|| deno_webstorage::get_version_error_class_name(e)) + .or_else(|| deno_webstorage::get_constraint_error_class_name(e)) + .or_else(|| deno_webstorage::get_invalid_state_error_class_name(e)) .or_else(|| deno_websocket::get_network_error_class_name(e)) .or_else(|| { e.downcast_ref::()