From ec4973a0ddae56c35d6f6e8da691ab34a67935c0 Mon Sep 17 00:00:00 2001 From: crowlkats Date: Thu, 3 Mar 2022 18:14:14 +0100 Subject: [PATCH 01/14] basic webidl --- ext/webidl/00_webidl.js | 10 + ext/websocket/01_websocket.js | 10 - ext/webstorage/02_indexeddb.js | 999 +++++++++++++++++++++++++++++++++ ext/webstorage/lib.rs | 1 + 4 files changed, 1010 insertions(+), 10 deletions(-) create mode 100644 ext/webstorage/02_indexeddb.js diff --git a/ext/webidl/00_webidl.js b/ext/webidl/00_webidl.js index de3221081b206a..164f36480d67d6 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..f2537b81ba30fd --- /dev/null +++ b/ext/webstorage/02_indexeddb.js @@ -0,0 +1,999 @@ +// 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 } = window.__bootstrap.event; + const {} = 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.converters["sequence or DOMString"], // TODO: nullable + 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, + }, + ], + ); + + const _result = Symbol("[[result]]"); + const _error = Symbol("[[error]]"); + const _source = Symbol("[[source]]"); + const _transaction = Symbol("[[transaction]]"); + const _processed = Symbol("[[processed]]"); + const _done = Symbol("[[done]]"); + // Ref: https://w3c.github.io/IndexedDB/#idbrequest + class IDBRequest extends EventTarget { + constructor() { + super(); + webidl.illegalConstructor(); + } + + [_processed]; + [_done] = false; + + [_result]; + get result() { + webidl.assertBranded(this, IDBRequestPrototype); + if (!this[_done]) { + throw new DOMException("", "InvalidStateError"); // TODO + } + return this[_result]; // TODO: or undefined if the request resulted in an error + } + + [_error] = null; + get error() { + webidl.assertBranded(this, IDBRequestPrototype); + if (!this[_done]) { + throw new DOMException("", "InvalidStateError"); // TODO + } + 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); + + // 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, + }); + } + + // TODO + } + + // 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", + }); + + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbfactory-databases + databases() { + webidl.assertBranded(this, IDBFactoryPrototype); + + // TODO + } + + // 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", + }); + + // TODO + } + } + webidl.configurePrototype(IDBFactory); + const IDBFactoryPrototype = IDBFactory.prototype; + + const _name = Symbol("[[name]]"); + const _version = Symbol("[[version]]"); + // Ref: https://w3c.github.io/IndexedDB/#idbdatabase + // TODO: finalizationRegistry + class IDBDatabase extends EventTarget { + 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); + // TODO + } + + // 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", + }); + + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-close + close() { + webidl.assertBranded(this, IDBDatabasePrototype); + // TODO + } + + // 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", + }); + + // TODO + } + + // 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", + }); + + // TODO + } + } + 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/#idbobjectstore + class IDBObjectStore { + constructor() { + webidl.illegalConstructor(); + } + + [_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", + }); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-keypath + get keyPath() { + webidl.assertBranded(this, IDBObjectStorePrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-indexnames + get indexNames() { + webidl.assertBranded(this, IDBObjectStorePrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-transaction + get transaction() { + webidl.assertBranded(this, IDBObjectStorePrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-autoincrement + get autoIncrement() { + webidl.assertBranded(this, IDBObjectStorePrototype); + // TODO + } + + // 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", + }); + + // TODO + } + + // 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", + }); + + // TODO + } + + // 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", + }); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-clear + clear() { + webidl.assertBranded(this, IDBObjectStorePrototype); + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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, + }); + } + // TODO + } + + // 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, + }); + } + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + } + webidl.configurePrototype(IDBObjectStore); + const IDBObjectStorePrototype = IDBObjectStore.prototype; + + // Ref: https://w3c.github.io/IndexedDB/#idbindex + class IDBIndex { + constructor() { + webidl.illegalConstructor(); + } + + [_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", + }); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-objectstore + get objectStore() { + webidl.assertBranded(this, IDBIndexPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-keypath + get keyPath() { + webidl.assertBranded(this, IDBIndexPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-multientry + get multiEntry() { + webidl.assertBranded(this, IDBIndexPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-unique + get unique() { + webidl.assertBranded(this, IDBIndexPrototype); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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, + }); + } + // TODO + } + + // 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, + }); + } + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + } + webidl.configurePrototype(IDBIndex); + const IDBIndexPrototype = IDBIndex.prototype; + + // Ref: https://w3c.github.io/IndexedDB/#idbkeyrange + class IDBKeyRange { + constructor() { + webidl.illegalConstructor(); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-lower + get lower() { + webidl.assertBranded(this, IDBKeyRangePrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-upper + get upper() { + webidl.assertBranded(this, IDBKeyRangePrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-loweropen + get lowerOpen() { + webidl.assertBranded(this, IDBKeyRangePrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-upperopen + get upperOpen() { + webidl.assertBranded(this, IDBKeyRangePrototype); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + 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", + }); + // TODO + } + } + webidl.configurePrototype(IDBKeyRange); + const IDBKeyRangePrototype = IDBKeyRange.prototype; + + // Ref: https://w3c.github.io/IndexedDB/#idbcursor + class IDBCursor { + constructor() { + webidl.illegalConstructor(); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-source + get source() { + webidl.assertBranded(this, IDBCursorPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-direction + get direction() { + webidl.assertBranded(this, IDBCursorPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-key + get key() { + webidl.assertBranded(this, IDBCursorPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-primarykey + get primaryKey() { + webidl.assertBranded(this, IDBCursorPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-request + get request() { + webidl.assertBranded(this, IDBCursorPrototype); + // TODO + } + + // 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, + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // 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", + }); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-delete + delete() { + webidl.assertBranded(this, IDBCursorPrototype); + // TODO + } + } + webidl.configurePrototype(IDBCursor); + const IDBCursorPrototype = IDBCursor.prototype; + + // Ref: https://w3c.github.io/IndexedDB/#idbtransaction + class IDBTransaction extends EventTarget { + constructor() { + super(); + webidl.illegalConstructor(); + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstorenames + get objectStoreNames() { + webidl.assertBranded(this, IDBTransactionPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-mode + get mode() { + webidl.assertBranded(this, IDBTransactionPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-durability + get durability() { + webidl.assertBranded(this, IDBTransactionPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-db + get db() { + webidl.assertBranded(this, IDBTransactionPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-error + get error() { + webidl.assertBranded(this, IDBTransactionPrototype); + // TODO + } + + // 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", + }); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-commit + commit() { + webidl.assertBranded(this, IDBTransactionPrototype); + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-abort + abort() { + webidl.assertBranded(this, IDBTransactionPrototype); + // TODO + } + } + 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/lib.rs b/ext/webstorage/lib.rs index ef053d2b969274..362b30defd195d 100644 --- a/ext/webstorage/lib.rs +++ b/ext/webstorage/lib.rs @@ -24,6 +24,7 @@ pub fn init(origin_storage_dir: Option) -> Extension { .js(include_js_files!( prefix "deno:ext/webstorage", "01_webstorage.js", + "02_webstorage.js", )) .ops(vec![ ("op_webstorage_length", op_sync(op_webstorage_length)), From 276afec1f8d5429bc911430ff323050d970cf5d0 Mon Sep 17 00:00:00 2001 From: crowlkats Date: Mon, 7 Mar 2022 13:08:08 +0100 Subject: [PATCH 02/14] work --- ext/webstorage/02_indexeddb.js | 213 +++++++++++++++++++++- ext/webstorage/02_indexeddb_types.d.ts | 9 + ext/webstorage/indexeddb.rs | 193 ++++++++++++++++++++ ext/webstorage/lib.rs | 234 ++++++------------------- ext/webstorage/webstorage.rs | 187 ++++++++++++++++++++ runtime/errors.rs | 1 + 6 files changed, 648 insertions(+), 189 deletions(-) create mode 100644 ext/webstorage/02_indexeddb_types.d.ts create mode 100644 ext/webstorage/indexeddb.rs create mode 100644 ext/webstorage/webstorage.rs diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index f2537b81ba30fd..295d2ddfe8f07b 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -7,7 +7,7 @@ const webidl = window.__bootstrap.webidl; const { DOMException } = window.__bootstrap.domException; const { defineEventHandler } = window.__bootstrap.event; - const {} = window.__bootstrap.primordials; + const { NumberIsNaN, ArrayIsArray, Date, DatePrototypeGetMilliseconds, Set, SetPrototypeHas, SetPrototypeAdd, MathMin } = window.__bootstrap.primordials; webidl.converters.IDBTransactionMode = webidl.createEnumConverter( "IDBTransactionMode", @@ -80,6 +80,132 @@ ], ); + // Ref: https://w3c.github.io/IndexedDB/#convert-a-value-to-a-key + /** + * @param input {any} + * @param seen {Set} + * @returns {(Key | null)} + */ + 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 () { // TODO: is a buffer source type + return { + type: "binary", + value: input.slice(), + } + } 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 { + return null; + } + } + + // 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 === "date") { + assert(tb === "date"); + return 1; + } else { + 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 (va < vb) { + return -1; + } else { + return 0; + } + } + case "binary": { + // TODO + } + 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; + } + } + } + } + const _result = Symbol("[[result]]"); const _error = Symbol("[[error]]"); const _source = Symbol("[[source]]"); @@ -149,6 +275,9 @@ webidl.configurePrototype(IDBOpenDBRequest); + /** @type {Set} */ + const connections = new Set(); + // Ref: https://w3c.github.io/IndexedDB/#idbfactory class IDBFactory { constructor() { @@ -172,7 +301,60 @@ }); } - // TODO + if (version === 0) { + throw new TypeError(); // TODO + } + + const request = webidl.createBranded(IDBOpenDBRequest); + + try { + const [newVersion, dbVersion] = core.opSync("op_indexeddb_open", name, version); + const connection = webidl.createBranded(IDBDatabase); + connection[_name] = name; + // TODO: connection[_version] = newVersion; + if (dbVersion < newVersion) { + for (const conn of connections.values()) { + if (!conn[_closePending]) { + conn.dispatchEvent(new IDBVersionChangeEvent("versionchange", { + bubbles: false, + cancelable: false, + oldVersion: dbVersion, + newVersion, + })); + } + } + // TODO: why should connections close? + for (const conn of connections.values()) { + if (!conn[_closePending]) { + request.dispatchEvent(new IDBVersionChangeEvent("blocked", { + bubbles: false, + cancelable: false, + oldVersion: dbVersion, + newVersion, + })); + break; + } + } + // Ref: https://w3c.github.io/IndexedDB/#upgrade-transaction-steps + // TODO: Wait until all connections in openConnections are closed. + const transaction; // TODO + // TODO + + } + request[_result] = connection; + 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 @@ -185,6 +367,8 @@ context: "Argument 1", }); + const request = webidl.createBranded(IDBOpenDBRequest); + // TODO } @@ -192,7 +376,12 @@ databases() { webidl.assertBranded(this, IDBFactoryPrototype); - // TODO + return Promise.resolve([...connections.values()].map((db) => { + return { + name: db.name, + version: db.version, + }; + })); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbfactory-cmp @@ -210,7 +399,16 @@ context: "Argument 2", }); - // TODO + 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); @@ -218,9 +416,16 @@ const _name = Symbol("[[name]]"); const _version = Symbol("[[version]]"); + const _closePending = Symbol("[[closePending]]"); + const _objectStores = Symbol("[[objectStores]]"); // Ref: https://w3c.github.io/IndexedDB/#idbdatabase // TODO: finalizationRegistry class IDBDatabase extends EventTarget { + /** @type {boolean} */ + [_closePending] = false; + /** @type {Set} */ + [_objectStores] = new Set(); + constructor() { super(); webidl.illegalConstructor(); diff --git a/ext/webstorage/02_indexeddb_types.d.ts b/ext/webstorage/02_indexeddb_types.d.ts new file mode 100644 index 00000000000000..79ba531bf8ab22 --- /dev/null +++ b/ext/webstorage/02_indexeddb_types.d.ts @@ -0,0 +1,9 @@ +// 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; +} diff --git a/ext/webstorage/indexeddb.rs b/ext/webstorage/indexeddb.rs new file mode 100644 index 00000000000000..05086a4299e1a0 --- /dev/null +++ b/ext/webstorage/indexeddb.rs @@ -0,0 +1,193 @@ +use crate::{DomExceptionNotSupportedError, DomExceptionVersionError, OriginStorageDir}; +use deno_core::error::AnyError; +use deno_core::{OpState, Resource, ResourceId}; +use rusqlite::{params, Connection, OptionalExtension}; +use std::borrow::Cow; +use std::cmp::Ordering; +use std::collections::HashMap; +use std::rc::Rc; + +fn create_file_table(conn: &Connection) -> Result<(), AnyError> { + let statements = r#" + CREATE TABLE IF NOT EXISTS file ( + id INTEGER PRIMARY KEY, + refcount INTEGER NOT NULL + ); + + CREATE TRIGGER object_data_insert_trigger + AFTER INSERT ON object_data + FOR EACH ROW + WHEN NEW.file_ids IS NOT NULL + BEGIN + SELECT update_refcount(NULL, NEW.file_ids); + + CREATE TRIGGER object_data_update_trigger + AFTER UPDATE OF file_ids ON object_data + FOR EACH ROW + WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL + BEGIN + SELECT update_refcount(OLD.file_ids, NEW.file_ids); + + CREATE TRIGGER object_data_delete_trigger + AFTER DELETE ON object_data + FOR EACH ROW WHEN OLD.file_ids IS NOT NULL + BEGIN + SELECT update_refcount(OLD.file_ids, NULL); + + CREATE TRIGGER file_update_trigger + AFTER UPDATE ON file + FOR EACH ROW WHEN NEW.refcount = 0 + BEGIN + DELETE FROM file WHERE id = OLD.id; + "#; + conn.execute_batch(statements)?; + Ok(()) +} + +fn create_table(conn: &Connection) -> Result<(), AnyError> { + let statements = r#" + CREATE TABLE database ( + name TEXT PRIMARY KEY, + version INTEGER NOT NULL DEFAULT 0 + ) WITHOUT ROWID; + + CREATE TABLE object_store ( + id INTEGER PRIMARY KEY, + auto_increment INTEGER NOT NULL DEFAULT 0, + name TEXT NOT NULL, + key_path TEXT + ); + + CREATE TABLE object_store_index ( + id INTEGER PRIMARY KEY, + object_store_id INTEGER NOT NULL, + database_name TEXT NOT NULL, + name TEXT NOT NULL, + key_path TEXT NOT NULL, + unique_index INTEGER NOT NULL, + multientry INTEGER NOT NULL, + FOREIGN KEY (object_store_id) + REFERENCES object_store(id) + FOREIGN KEY (database_name) + REFERENCES database(name) + ); + + CREATE TABLE object_data ( + object_store_id INTEGER NOT NULL, + key BLOB NOT NULL, + index_data_values BLOB DEFAULT NULL, + file_ids TEXT, + data BLOB NOT NULL, + PRIMARY KEY (object_store_id, key), + FOREIGN KEY (object_store_id) + REFERENCES object_store(id) + ) WITHOUT ROWID; + + CREATE TABLE index_data ( + index_id INTEGER NOT NULL, + value BLOB NOT NULL, + object_data_key BLOB NOT NULL, + object_store_id INTEGER NOT NULL, + value_locale BLOB, + PRIMARY KEY (index_id, value, object_data_key), + FOREIGN KEY (index_id) + REFERENCES object_store_index(id), + FOREIGN KEY (object_store_id, object_data_key) + REFERENCES object_data(object_store_id, key) + ) WITHOUT ROWID; + + CREATE INDEX index_data_value_locale_index + ON index_data (index_id, value_locale, object_data_key, value) + WHERE value_locale IS NOT NULL; + + CREATE TABLE unique_index_data ( + index_id INTEGER NOT NULL, + value BLOB NOT NULL, + object_store_id INTEGER NOT NULL, + object_data_key BLOB NOT NULL, + value_locale BLOB, + PRIMARY KEY (index_id, value), + FOREIGN KEY (index_id) + REFERENCES object_store_index(id), + FOREIGN KEY (object_store_id, object_data_key) + REFERENCES object_data(object_store_id, key) + ) WITHOUT ROWID; + + CREATE INDEX unique_index_data_value_locale_index + ON unique_index_data (index_id, value_locale, object_data_key, value) + WHERE value_locale IS NOT NULL + "#; + conn.execute_batch(statements)?; + Ok(()) +} + +struct Database { + name: String, + version: u64, +} + +struct IndexedDbConnection { + conn: Connection, + close_pending: bool, +} + +pub struct IndexedDbManager(Connection); +pub struct IndexedDbResource(Connection); + +impl Resource for IndexedDbResource { + fn name(&self) -> Cow { + "indexedDb".into() + } +} + +// Ref: https://w3c.github.io/IndexedDB/#open-a-database +pub fn op_indexeddb_open( + state: &mut OpState, + name: String, + version: Option, +) -> Result<(u64, u64), AnyError> { + if state.try_borrow::().is_none() { + 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)?; + create_table(&conn)?; + + conn.set_prepared_statement_cache_capacity(128); + state.put(IndexedDbManager(conn)); + } + + let idbmanager = state.borrow::(); + let conn = &idbmanager.0; + let mut stmt = conn.prepare_cached("SELECT version FROM data 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.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)) +} diff --git a/ext/webstorage/lib.rs b/ext/webstorage/lib.rs index 362b30defd195d..656863a05c4487 100644 --- a/ext/webstorage/lib.rs +++ b/ext/webstorage/lib.rs @@ -2,6 +2,9 @@ // 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_sync; @@ -17,8 +20,6 @@ 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!( @@ -27,16 +28,29 @@ pub fn init(origin_storage_dir: Option) -> Extension { "02_webstorage.js", )) .ops(vec![ - ("op_webstorage_length", op_sync(op_webstorage_length)), - ("op_webstorage_key", op_sync(op_webstorage_key)), - ("op_webstorage_set", op_sync(op_webstorage_set)), - ("op_webstorage_get", op_sync(op_webstorage_get)), - ("op_webstorage_remove", op_sync(op_webstorage_remove)), - ("op_webstorage_clear", op_sync(op_webstorage_clear)), + // WebStorage + ( + "op_webstorage_length", + op_sync(webstorage::op_webstorage_length), + ), + ("op_webstorage_key", op_sync(webstorage::op_webstorage_key)), + ("op_webstorage_set", op_sync(webstorage::op_webstorage_set)), + ("op_webstorage_get", op_sync(webstorage::op_webstorage_get)), + ( + "op_webstorage_remove", + op_sync(webstorage::op_webstorage_remove), + ), + ( + "op_webstorage_clear", + op_sync(webstorage::op_webstorage_clear), + ), ( "op_webstorage_iterate_keys", - op_sync(op_webstorage_iterate_keys), + op_sync(webstorage::op_webstorage_iterate_keys), ), + + // IndexedDb + ("op_indexeddb_open", op_sync(indexeddb::op_indexeddb_open)) ]) .state(move |state| { if let Some(origin_storage_dir) = &origin_storage_dir { @@ -51,208 +65,58 @@ 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)); - } - - &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) -} - -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) -} - -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, +#[derive(Debug)] +pub struct DomExceptionNotSupportedError { + pub msg: String, } -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 DomExceptionNotSupportedError { + pub fn new(msg: &str) -> Self { + DomExceptionNotSupportedError { + 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(()) -} - -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) -} - -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(()) } -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(()) +impl fmt::Display for DomExceptionNotSupportedError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.pad(&self.msg) + } } -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(); +impl std::error::Error for DomExceptionNotSupportedError {} - Ok(keys) +pub fn get_not_supported_error_class_name( + e: &AnyError, +) -> Option<&'static str> { + e.downcast_ref::() + .map(|_| "DOMExceptionNotSupportedError") } #[derive(Debug)] -pub struct DomExceptionNotSupportedError { +pub struct DomExceptionVersionError { pub msg: String, } -impl DomExceptionNotSupportedError { +impl DomExceptionVersionError { pub fn new(msg: &str) -> Self { - DomExceptionNotSupportedError { + DomExceptionVersionError { msg: msg.to_string(), } } } -impl fmt::Display for DomExceptionNotSupportedError { +impl fmt::Display for DomExceptionVersionError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.pad(&self.msg) } } -impl std::error::Error for DomExceptionNotSupportedError {} +impl std::error::Error for DomExceptionVersionError {} -pub fn get_not_supported_error_class_name( +pub fn get_version_error_class_name( e: &AnyError, ) -> Option<&'static str> { - e.downcast_ref::() - .map(|_| "DOMExceptionNotSupportedError") + e.downcast_ref::() + .map(|_| "DOMExceptionVersionError") } diff --git a/ext/webstorage/webstorage.rs b/ext/webstorage/webstorage.rs new file mode 100644 index 00000000000000..4378adbdc3f265 --- /dev/null +++ b/ext/webstorage/webstorage.rs @@ -0,0 +1,187 @@ +use super::DomExceptionNotSupportedError; +use super::OriginStorageDir; +use deno_core::error::AnyError; +use deno_core::OpState; +use rusqlite::params; +use rusqlite::Connection; +use rusqlite::OptionalExtension; + +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) +} + +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) +} + +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, +} + +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(()) +} + +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) +} + +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(()) +} + +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(()) +} + +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..6265e9f7af43e4 100644 --- a/runtime/errors.rs +++ b/runtime/errors.rs @@ -157,6 +157,7 @@ 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_websocket::get_network_error_class_name(e)) .or_else(|| { e.downcast_ref::() From 0f52deb942387387797358f55e84c7c130ce42a8 Mon Sep 17 00:00:00 2001 From: crowlkats Date: Tue, 8 Mar 2022 06:43:34 +0100 Subject: [PATCH 03/14] work --- ext/webstorage/02_indexeddb.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index 295d2ddfe8f07b..a6924c08867f98 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -275,7 +275,18 @@ webidl.configurePrototype(IDBOpenDBRequest); - /** @type {Set} */ + class Connection { + /** @type {Set} */ + databases = new Set(); + /** @type {number} */ + version; + /** @type {boolean} */ + closePending = false; + /** @type */ + objectStoreSet; + } + + /** @type {Set} */ const connections = new Set(); // Ref: https://w3c.github.io/IndexedDB/#idbfactory @@ -418,6 +429,7 @@ const _version = Symbol("[[version]]"); const _closePending = Symbol("[[closePending]]"); const _objectStores = Symbol("[[objectStores]]"); + const _connection = Symbol("[[connection]]"); // Ref: https://w3c.github.io/IndexedDB/#idbdatabase // TODO: finalizationRegistry class IDBDatabase extends EventTarget { @@ -425,6 +437,8 @@ [_closePending] = false; /** @type {Set} */ [_objectStores] = new Set(); + /** @type {Connection} */ + [_connection]; constructor() { super(); From cc3776224d469f4860a493fa57dfa91ad5faeec1 Mon Sep 17 00:00:00 2001 From: crowlkats Date: Thu, 10 Mar 2022 12:46:46 +0100 Subject: [PATCH 04/14] work --- Cargo.lock | 1 + ext/webstorage/02_indexeddb.js | 212 ++++++++++++++---- ext/webstorage/02_indexeddb_types.d.ts | 2 + ext/webstorage/Cargo.toml | 1 + ext/webstorage/indexeddb/idbtrait.rs | 7 + .../{indexeddb.rs => indexeddb/mod.rs} | 84 ++++++- ext/webstorage/indexeddb/sqlite_idb.rs | 0 ext/webstorage/lib.rs | 7 +- 8 files changed, 257 insertions(+), 57 deletions(-) create mode 100644 ext/webstorage/indexeddb/idbtrait.rs rename ext/webstorage/{indexeddb.rs => indexeddb/mod.rs} (70%) create mode 100644 ext/webstorage/indexeddb/sqlite_idb.rs diff --git a/Cargo.lock b/Cargo.lock index c160cf8d57bff2..ce8871e2681510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1139,6 +1139,7 @@ dependencies = [ name = "deno_webstorage" version = "0.33.0" dependencies = [ + "async-trait", "deno_core", "deno_web", "rusqlite", diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index a6924c08867f98..331c792ebdca01 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -7,7 +7,7 @@ const webidl = window.__bootstrap.webidl; const { DOMException } = window.__bootstrap.domException; const { defineEventHandler } = window.__bootstrap.event; - const { NumberIsNaN, ArrayIsArray, Date, DatePrototypeGetMilliseconds, Set, SetPrototypeHas, SetPrototypeAdd, MathMin } = window.__bootstrap.primordials; + const { NumberIsNaN, ArrayIsArray, Date, SafeArrayIterator, DatePrototypeGetMilliseconds, MapPrototypeGet, MapPrototypeDelete, ArrayPrototypeSort, Set, SetPrototypeHas, SetPrototypeAdd, MathMin, MapPrototypeKeys } = window.__bootstrap.primordials; webidl.converters.IDBTransactionMode = webidl.createEnumConverter( "IDBTransactionMode", @@ -80,12 +80,12 @@ ], ); - // Ref: https://w3c.github.io/IndexedDB/#convert-a-value-to-a-key /** * @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; @@ -206,6 +206,15 @@ } } + // Ref: https://w3c.github.io/IndexedDB/#valid-key-path + function isValidKeyPath(key) { + if (typeof key === "string" && key.length === 0) { + return true; + } else if () { + // TODO + } + } + const _result = Symbol("[[result]]"); const _error = Symbol("[[error]]"); const _source = Symbol("[[source]]"); @@ -275,20 +284,6 @@ webidl.configurePrototype(IDBOpenDBRequest); - class Connection { - /** @type {Set} */ - databases = new Set(); - /** @type {number} */ - version; - /** @type {boolean} */ - closePending = false; - /** @type */ - objectStoreSet; - } - - /** @type {Set} */ - const connections = new Set(); - // Ref: https://w3c.github.io/IndexedDB/#idbfactory class IDBFactory { constructor() { @@ -386,13 +381,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbfactory-databases databases() { webidl.assertBranded(this, IDBFactoryPrototype); - - return Promise.resolve([...connections.values()].map((db) => { - return { - name: db.name, - version: db.version, - }; - })); + return core.opAsync("op_indexeddb_list_databases"); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbfactory-cmp @@ -425,10 +414,41 @@ webidl.configurePrototype(IDBFactory); const IDBFactoryPrototype = IDBFactory.prototype; + // Ref: https://w3c.github.io/IndexedDB/#database-connection + class Connection { + /** @type {Set} */ + databases = new Set(); + /** @type {number} */ + version; + /** @type {boolean} */ + closePending = false; + /** @type {Map} */ + objectStoreSet; + + /** + * @param forced {boolean} + */ + // Ref: https://w3c.github.io/IndexedDB/#close-a-database-connection + close(forced) { + this.closePending = true; + if (forced) { + // TODO: 2 + } + // TODO: 3. + if (forced) { + // TODO: 4. + } + } + } + + /** @type {Set} */ + const connections = new Set(); + const _name = Symbol("[[name]]"); const _version = Symbol("[[version]]"); const _closePending = Symbol("[[closePending]]"); const _objectStores = Symbol("[[objectStores]]"); + const _upgradeTransaction = Symbol("[[upgradeTransaction]]"); const _connection = Symbol("[[connection]]"); // Ref: https://w3c.github.io/IndexedDB/#idbdatabase // TODO: finalizationRegistry @@ -437,6 +457,8 @@ [_closePending] = false; /** @type {Set} */ [_objectStores] = new Set(); + /** @type {(IDBTransaction | null)} */ + [_upgradeTransaction] = null; /** @type {Connection} */ [_connection]; @@ -462,7 +484,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-objectstorenames get objectStoreNames() { webidl.assertBranded(this, IDBDatabasePrototype); - // TODO + return ArrayPrototypeSort([...new SafeArrayIterator(MapPrototypeKeys(this[_connection].objectStoreSet))]); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-transaction @@ -492,7 +514,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-close close() { webidl.assertBranded(this, IDBDatabasePrototype); - // TODO + this[_connection].close(false); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore @@ -509,7 +531,30 @@ context: "Argument 2", }); + if (this[_upgradeTransaction] === null) { + throw new DOMException("", "InvalidStateError"); // TODO + } + + if (this[_upgradeTransaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); // TODO + } + + const keyPath = options.keyPath ?? null; + + if (options.keyPath !== null && !isValidKeyPath(options.keyPath)) { + throw new DOMException("", "SyntaxError"); // TODO + } + + if ((typeof options.keyPath === "string" && options.keyPath.length === 0) || ArrayIsArray(options.keyPath)) { + throw new DOMException("", "InvalidAccessError"); // TODO + } + // TODO + const objectStore = webidl.createBranded(IDBObjectStore); + objectStore[_name] = name; + objectStore[_keyPath] = keyPath; + objectStore[_autoIncrement] = options.autoIncrement; + return objectStore; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-deleteobjectstore @@ -522,7 +567,21 @@ context: "Argument 1", }); - // TODO + if (this[_upgradeTransaction] === null) { + throw new DOMException("", "InvalidStateError"); // TODO: error message + } + + if (this[_upgradeTransaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); // TODO: error message + } + + const store = MapPrototypeGet(this[_connection].objectStoreSet, name); + if (store === undefined) { + throw new DOMException("", "NotFoundError"); // TODO: error message + } + MapPrototypeDelete(this[_connection].objectStoreSet, name); + + // TODO 6. } } defineEventHandler(IDBDatabase.prototype, "abort"); @@ -533,6 +592,8 @@ webidl.configurePrototype(IDBDatabase); const IDBDatabasePrototype = IDBDatabase.prototype; + const _autoIncrement = Symbol("[[autoIncrement]]"); + const _keyPath = Symbol("[[keyPath]]"); // Ref: https://w3c.github.io/IndexedDB/#idbobjectstore class IDBObjectStore { constructor() { @@ -556,6 +617,7 @@ // TODO } + [_keyPath]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-keypath get keyPath() { webidl.assertBranded(this, IDBObjectStorePrototype); @@ -568,16 +630,18 @@ // TODO } + [_transaction]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-transaction get transaction() { webidl.assertBranded(this, IDBObjectStorePrototype); - // TODO + return this[_transaction]; } + [_autoIncrement]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-autoincrement get autoIncrement() { webidl.assertBranded(this, IDBObjectStorePrototype); - // TODO + return this[_autoIncrement]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-put @@ -930,34 +994,64 @@ 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); - // TODO + return this[_lowerBound]; } + [_upperBound]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-upper get upper() { webidl.assertBranded(this, IDBKeyRangePrototype); - // TODO + return this[_upperBound]; } + [_lowerOpen]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-loweropen get lowerOpen() { webidl.assertBranded(this, IDBKeyRangePrototype); - // TODO + return this[_lowerOpen]; } + [_upperOpen]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-upperopen get upperOpen() { webidl.assertBranded(this, IDBKeyRangePrototype); - // TODO + return this[_upperOpen]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-only @@ -968,7 +1062,11 @@ prefix, context: "Argument 1", }); - // TODO + const key = valueToKey(value); + if (key === null) { + throw new DOMException("", "DataError"); // TODO: error + } + return createRange(key, key); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-lowerbound @@ -983,7 +1081,11 @@ prefix, context: "Argument 2", }); - // TODO + const lowerKey = valueToKey(lower); + if (lowerKey === null) { + throw new DOMException("", "DataError"); // TODO: error + } + return createRange(lowerKey, null, open, true); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-upperbound @@ -998,7 +1100,11 @@ prefix, context: "Argument 2", }); - // TODO + const upperKey = valueToKey(upper); + if (upperKey === null) { + throw new DOMException("", "DataError"); // TODO: error + } + return createRange(null, upperKey, true, open); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbkeyrange-bound @@ -1021,7 +1127,18 @@ prefix, context: "Argument 4", }); - // TODO + const lowerKey = valueToKey(lower); + if (lowerKey === null) { + throw new DOMException("", "DataError"); // TODO: error + } + const upperKey = valueToKey(upper); + if (upperKey === null) { + throw new DOMException("", "DataError"); // TODO: error + } + if (compareTwoKeys(lowerKey, upperKey) === 1) { + throw new DOMException("", "DataError"); // TODO: error + } + return createRange(lowerKey, upperKey, lowerOpen, upperOpen); } includes(key) { @@ -1032,7 +1149,11 @@ prefix, context: "Argument 1", }); - // TODO + const keyVal = valueToKey(key); + if (keyVal === null) { + throw new DOMException("", "DataError"); // TODO: error + } + return keyInRange(this, key); } } webidl.configurePrototype(IDBKeyRange); @@ -1135,8 +1256,19 @@ webidl.configurePrototype(IDBCursor); const IDBCursorPrototype = IDBCursor.prototype; + const _requestList = Symbol("[[requestList]]"); + const _state = Symbol("[[state]]"); + const _mode = Symbol("[[mode]]"); + const _db = Symbol("[[db]]"); // Ref: https://w3c.github.io/IndexedDB/#idbtransaction class IDBTransaction extends EventTarget { + [_requestList] = []; + /** @type {TransactionState} */ + [_state] = "active"; + [_mode]; + [_error]; + [_db]; + constructor() { super(); webidl.illegalConstructor(); @@ -1151,7 +1283,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-mode get mode() { webidl.assertBranded(this, IDBTransactionPrototype); - // TODO + return this[_mode]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-durability @@ -1163,13 +1295,13 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-db get db() { webidl.assertBranded(this, IDBTransactionPrototype); - // TODO + return this[_db]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-error get error() { webidl.assertBranded(this, IDBTransactionPrototype); - // TODO + return this[_error]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore diff --git a/ext/webstorage/02_indexeddb_types.d.ts b/ext/webstorage/02_indexeddb_types.d.ts index 79ba531bf8ab22..95d4513caadae5 100644 --- a/ext/webstorage/02_indexeddb_types.d.ts +++ b/ext/webstorage/02_indexeddb_types.d.ts @@ -7,3 +7,5 @@ interface Key { // deno-lint-ignore no-explicit-any value: any; } + +type TransactionState = "active" | "inactive" | "committing" | "finished"; diff --git a/ext/webstorage/Cargo.toml b/ext/webstorage/Cargo.toml index b9408fde08f728..27427a56cb9817 100644 --- a/ext/webstorage/Cargo.toml +++ b/ext/webstorage/Cargo.toml @@ -18,3 +18,4 @@ deno_core = { version = "0.120.0", path = "../../core" } deno_web = { version = "0.69.0", path = "../web" } rusqlite = { version = "0.25.3", features = ["unlock_notify", "bundled"] } serde = { version = "1.0.129", features = ["derive"] } +async-trait = "0.1.52" diff --git a/ext/webstorage/indexeddb/idbtrait.rs b/ext/webstorage/indexeddb/idbtrait.rs new file mode 100644 index 00000000000000..f4bcec65d73f79 --- /dev/null +++ b/ext/webstorage/indexeddb/idbtrait.rs @@ -0,0 +1,7 @@ +use async_trait::async_trait; + +#[async_trait] +trait IDB { + async fn open_database(&self, name: String, version: Option) -> (u64, u64); + async fn list_databases(&self) -> Vec; +} diff --git a/ext/webstorage/indexeddb.rs b/ext/webstorage/indexeddb/mod.rs similarity index 70% rename from ext/webstorage/indexeddb.rs rename to ext/webstorage/indexeddb/mod.rs index 05086a4299e1a0..1e6a2fe3b6fda3 100644 --- a/ext/webstorage/indexeddb.rs +++ b/ext/webstorage/indexeddb/mod.rs @@ -1,4 +1,9 @@ -use crate::{DomExceptionNotSupportedError, DomExceptionVersionError, OriginStorageDir}; +mod idbtrait; +mod sqlite_idb; + +use crate::{ + DomExceptionNotSupportedError, DomExceptionVersionError, OriginStorageDir, +}; use deno_core::error::AnyError; use deno_core::{OpState, Resource, ResourceId}; use rusqlite::{params, Connection, OptionalExtension}; @@ -169,25 +174,80 @@ pub fn op_indexeddb_open( let idbmanager = state.borrow::(); let conn = &idbmanager.0; - let mut stmt = conn.prepare_cached("SELECT version FROM data WHERE name = ?")?; + 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)?, - })) + .query_row(params![name], |row| { + Ok(Database { + name: row.get(0)?, + version: row.get(1)?, + }) + }) .optional()?; - let version = version.or_else(|| db.map(|db|db.version)).unwrap_or(1); + let version = version.or_else(|| db.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 (?)")?; + let mut stmt = + conn.prepare_cached("INSERT INTO database (name) VALUES (?)")?; stmt.execute(params![name])?; // TODO: 6. DOMException - Database { - name, - version: 0, - } + Database { name, version: 0 } }; Ok((version, db.version)) } + +// Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore +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).unwrap()).collect::>()?; + Ok(names) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateObjectStoreArgs { + database_name: String, + name: String, + key_path: Option, + auto_increment: bool, +} + +// Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore +pub fn op_indexeddb_database_create_object_store( + state: &mut OpState, + args: CreateObjectStoreArgs, + _: (), +) -> Result<(), AnyError> { + let conn = &state.borrow::().0; + + // TODO: this might be doable on the JS side + let mut stmt = conn.prepare_cached( + "SELECT * FROM object_store_index WHERE name = ? AND database_name = ?", + )?; + if stmt.exists(params![args.name, args.database_name])? { + return Err(); + } + + // TODO: autoIncrement + let mut stmt = conn.prepare_cached( + "INSERT INTO object_store (name, keyPath) VALUES (?, ?) RETURNING id", + )?; + let store_id: u64 = + stmt.query_row(params![args.name, args.key_path], |row| row.get(0))?; + + let mut stmt = conn.prepare_cached("INSERT INTO object_store_index (object_store_id, database_name, name, key_path) VALUES (?, ?, ?, ?)")?; + stmt.execute(params![ + store_id, + args.database_name, + args.name, + args.key_path + ])?; // TODO: more args needed + + Ok(()) +} diff --git a/ext/webstorage/indexeddb/sqlite_idb.rs b/ext/webstorage/indexeddb/sqlite_idb.rs new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/ext/webstorage/lib.rs b/ext/webstorage/lib.rs index 656863a05c4487..f7b4f6d547145b 100644 --- a/ext/webstorage/lib.rs +++ b/ext/webstorage/lib.rs @@ -48,9 +48,8 @@ pub fn init(origin_storage_dir: Option) -> Extension { "op_webstorage_iterate_keys", op_sync(webstorage::op_webstorage_iterate_keys), ), - // IndexedDb - ("op_indexeddb_open", op_sync(indexeddb::op_indexeddb_open)) + ("op_indexeddb_open", op_sync(indexeddb::op_indexeddb_open)), ]) .state(move |state| { if let Some(origin_storage_dir) = &origin_storage_dir { @@ -114,9 +113,7 @@ impl fmt::Display for DomExceptionVersionError { impl std::error::Error for DomExceptionVersionError {} -pub fn get_version_error_class_name( - e: &AnyError, -) -> Option<&'static str> { +pub fn get_version_error_class_name(e: &AnyError) -> Option<&'static str> { e.downcast_ref::() .map(|_| "DOMExceptionVersionError") } From acaf2b3ee5adc1430aa3ec298a49542080006108 Mon Sep 17 00:00:00 2001 From: crowlkats Date: Sun, 13 Mar 2022 13:21:12 +0100 Subject: [PATCH 05/14] work --- ext/webstorage/02_indexeddb.js | 61 ++++++++++++++++++++++++++------- ext/webstorage/indexeddb/mod.rs | 3 +- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index 331c792ebdca01..60b835c33085eb 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -532,11 +532,11 @@ }); if (this[_upgradeTransaction] === null) { - throw new DOMException("", "InvalidStateError"); // TODO + throw new DOMException("No upgrade transaction present", "InvalidStateError"); } if (this[_upgradeTransaction][_state] !== "active") { - throw new DOMException("", "TransactionInactiveError"); // TODO + throw new DOMException("Upgrade transaction is not active", "TransactionInactiveError"); } const keyPath = options.keyPath ?? null; @@ -549,8 +549,10 @@ throw new DOMException("", "InvalidAccessError"); // TODO } - // TODO + // TODO: call op_indexeddb_database_create_object_store + const objectStore = webidl.createBranded(IDBObjectStore); + objectStore[_transaction] = this[_upgradeTransaction]; objectStore[_name] = name; objectStore[_keyPath] = keyPath; objectStore[_autoIncrement] = options.autoIncrement; @@ -600,6 +602,9 @@ webidl.illegalConstructor(); } + /** @type {IDBTransaction} */ + [_transaction]; + [_name]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-name get name() { @@ -614,6 +619,21 @@ prefix: "Failed to set 'name' on 'IDBObjectStore'", context: "Argument 1", }); + + // TODO: 4. + + if (this[_transaction][_mode] !== "versionchange") { + throw new DOMException("", "InvalidStateError"); // TODO: error + } + + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); // TODO: error + } + + if (this[_name] === name) { + return; + } + // TODO } @@ -1064,7 +1084,7 @@ }); const key = valueToKey(value); if (key === null) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("Invalid key provided", "DataError"); } return createRange(key, key); } @@ -1083,7 +1103,7 @@ }); const lowerKey = valueToKey(lower); if (lowerKey === null) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("Invalid key provided", "DataError"); } return createRange(lowerKey, null, open, true); } @@ -1102,7 +1122,7 @@ }); const upperKey = valueToKey(upper); if (upperKey === null) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("Invalid key provided", "DataError"); } return createRange(null, upperKey, true, open); } @@ -1129,14 +1149,14 @@ }); const lowerKey = valueToKey(lower); if (lowerKey === null) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("Invalid lower key provided", "DataError"); } const upperKey = valueToKey(upper); if (upperKey === null) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("Invalid upper key provided", "DataError"); } if (compareTwoKeys(lowerKey, upperKey) === 1) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("Lower key is greater than upper key", "DataError"); } return createRange(lowerKey, upperKey, lowerOpen, upperOpen); } @@ -1151,7 +1171,7 @@ }); const keyVal = valueToKey(key); if (keyVal === null) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("Invalid key provided", "DataError"); } return keyInRange(this, key); } @@ -1159,22 +1179,30 @@ webidl.configurePrototype(IDBKeyRange); const IDBKeyRangePrototype = IDBKeyRange.prototype; + const _direction = Symbol("[[direction]]"); + const _request = Symbol("[[request]]"); // Ref: https://w3c.github.io/IndexedDB/#idbcursor class IDBCursor { constructor() { webidl.illegalConstructor(); } + /** @type {IDBTransaction} */ + [_transaction]; + + [_source]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-source get source() { webidl.assertBranded(this, IDBCursorPrototype); - // TODO + return this[_source]; } + /** @type {IDBCursorDirection} */ + [_direction]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-direction get direction() { webidl.assertBranded(this, IDBCursorPrototype); - // TODO + return this[_direction]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-key @@ -1189,10 +1217,11 @@ // TODO } + [_request]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-request get request() { webidl.assertBranded(this, IDBCursorPrototype); - // TODO + return this[_request]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-advance @@ -1205,6 +1234,12 @@ context: "Argument 1", enforceRange: true, }); + if (count === 0) { + throw new TypeError("Count cannot be 0"); + } + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); // TODO: error + } // TODO } diff --git a/ext/webstorage/indexeddb/mod.rs b/ext/webstorage/indexeddb/mod.rs index 1e6a2fe3b6fda3..d7362b91c4e581 100644 --- a/ext/webstorage/indexeddb/mod.rs +++ b/ext/webstorage/indexeddb/mod.rs @@ -234,7 +234,8 @@ pub fn op_indexeddb_database_create_object_store( return Err(); } - // TODO: autoIncrement + // TODO: 8. + let mut stmt = conn.prepare_cached( "INSERT INTO object_store (name, keyPath) VALUES (?, ?) RETURNING id", )?; From 2b23d7edc43f3543aa1f85df44f3c194f47bca50 Mon Sep 17 00:00:00 2001 From: crowlkats Date: Thu, 17 Mar 2022 17:55:17 +0100 Subject: [PATCH 06/14] work --- ext/web/02_event.js | 1 + ext/webstorage/02_indexeddb.js | 382 ++++++++++++++++++++++++- ext/webstorage/02_indexeddb_types.d.ts | 6 + ext/webstorage/indexeddb/idbtrait.rs | 15 +- ext/webstorage/indexeddb/mod.rs | 30 ++ ext/webstorage/indexeddb/sqlite_idb.rs | 17 ++ 6 files changed, 443 insertions(+), 8 deletions(-) diff --git a/ext/web/02_event.js b/ext/web/02_event.js index e85b4f5cbcf464..6c3bc3033c015e 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -1328,6 +1328,7 @@ setEventTargetData, }; window.__bootstrap.event = { + _canceledFlag, setIsTrusted, setTarget, defineEventHandler, diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index 60b835c33085eb..44b5ce28e67b79 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -6,8 +6,8 @@ const core = window.Deno.core; const webidl = window.__bootstrap.webidl; const { DOMException } = window.__bootstrap.domException; - const { defineEventHandler } = window.__bootstrap.event; - const { NumberIsNaN, ArrayIsArray, Date, SafeArrayIterator, DatePrototypeGetMilliseconds, MapPrototypeGet, MapPrototypeDelete, ArrayPrototypeSort, Set, SetPrototypeHas, SetPrototypeAdd, MathMin, MapPrototypeKeys } = window.__bootstrap.primordials; + const { defineEventHandler, _canceledFlag } = window.__bootstrap.event; + const { NumberIsNaN, ArrayIsArray, Date, SafeArrayIterator, ObjectPrototypeHasOwnProperty, DatePrototypeGetMilliseconds, MapPrototypeGet, MapPrototypeDelete, ArrayPrototypeSort, Set, SetPrototypeHas, SetPrototypeAdd, MathMin, MapPrototypeKeys } = window.__bootstrap.primordials; webidl.converters.IDBTransactionMode = webidl.createEnumConverter( "IDBTransactionMode", @@ -138,6 +138,26 @@ } } + // 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/#compare-two-keys function compareTwoKeys(a, b) { const { type: ta, value: va } = a; @@ -206,6 +226,23 @@ } } + // 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; // TODO: check + case "array": { + return key.value.map(keyToValue); + } + } + } + // Ref: https://w3c.github.io/IndexedDB/#valid-key-path function isValidKeyPath(key) { if (typeof key === "string" && key.length === 0) { @@ -215,6 +252,227 @@ } } + // Ref: https://w3c.github.io/IndexedDB/#check-that-a-key-could-be-injected-into-a-value + // TODO: check + 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"; + // TODO: 4. + transaction[_state] = "active"; + // TODO: 6. + } + + function abortTransaction(transaction, error) { + // TODO: 1. + 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[_processed] = true; + request[_done] = true; + request[_result] = undefined; + request[_error] = new DOMException("", "AbortError"); // TODO: error + request.dispatchEvent(new Event("error", { + bubbles: true, + cancelable: true, + })); + } + if (transaction[_mode] === "versionchange") { + // TODO: 6.1. + } + transaction.dispatchEvent(new Event("abort", { + bubbles: true, + })); + if (transaction[_mode] === "versionchange") { + // TODO: 6.3. + } + + } + + 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(keyPath[i], value); // TODO: seems spec could be wrong and arguments should be swapped + // TODO: 2. + if (key === _failure) { + return _failure; + } + result[i] = key; + // TODO: 6. + } + 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; + } + } + } + // TODO: 5.. + 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); + + (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 + errored = true; + } + } + request[_processed] = true; + 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: 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"; + // TODO: 8.2. + 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 + const event = new Event("success", { + bubbles: false, + cancelable: false, + }); + // TODO: legacyOutputDidListenersThrowFlag? + if (request[_transaction][_state] === "inactive") { + request[_transaction][_state] = "active"; + } + request.dispatchEvent(event); + if (request[_transaction][_state] === "active") { + request[_transaction][_state] = "inactive"; + // TODO: 8.2. + if (request[_transaction][_requestList].length === 0) { + commitTransaction(request[_transaction]); + } + } + } + })(); + return request; + } + + function commitTransaction(transaction) { + // TODO + } + const _result = Symbol("[[result]]"); const _error = Symbol("[[error]]"); const _source = Symbol("[[source]]"); @@ -594,8 +852,114 @@ webidl.configurePrototype(IDBDatabase); const IDBDatabasePrototype = IDBDatabase.prototype; + class Store { + /** @type {string} */ + name; + + /** @type {Database} */ + database; // TODO + + keyPath; // TODO + + /** @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"); // TODO + } + return this.current++; + }, + // Ref: https://w3c.github.io/IndexedDB/#possibly-update-the-key-generator + possiblyUpdate(key) { + if (key.type !== "number") { + return; + } + const value = MathMin(key.value, 9007199254740992); + // TODO: 4. + if (value >= this.current) { + this.current = value + 1; + } + } + } + } + } + } + + 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); + } + } + + // TODO: probably the rest should be an op. + } + + // Ref: https://w3c.github.io/IndexedDB/#add-or-put + function addOrPut(handle, value, key, noOverwrite) { + // TODO: 3. + + if (handle[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); // TODO: error + } + + if (handle[_transaction][_mode] !== "readonly") { + throw new DOMException("", "ReadOnlyError"); // TODO: error + } + + if (handle[_store].keyPath !== null && key !== undefined) { + throw new DOMException("", "DataError"); // TODO: error + } + + if (handle[_store].keyPath === null && handle[_store].keyGenerator === null && key === undefined) { + throw new DOMException("", "DataError"); // TODO: error + } + + if (key !== undefined) { + const r = valueToKey(key); + if (r === null) { + throw new DOMException("", "DataError"); // TODO: error + } + 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"); // TODO: error + } + if (kpk !== _failure) { + key = kpk; + } else { + if (handle[_store].keyGenerator === null) { + throw new DOMException("", "DataError"); // TODO: error + } else { + if (!checkKeyCanBeInjectedIntoValue(cloned, handle[_store].keyPath)) { + throw new DOMException("", "DataError"); // TODO: error + } + } + } + } + + return asynchronouslyExecuteRequest(handle, () => storeRecordIntoObjectStore(handle[_store], cloned, key, noOverwrite)); + } + const _autoIncrement = Symbol("[[autoIncrement]]"); const _keyPath = Symbol("[[keyPath]]"); + const _store = Symbol("[[store]]"); // Ref: https://w3c.github.io/IndexedDB/#idbobjectstore class IDBObjectStore { constructor() { @@ -604,6 +968,8 @@ /** @type {IDBTransaction} */ [_transaction]; + /** @type {Store} */ + [_store]; [_name]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-name @@ -634,7 +1000,13 @@ return; } - // TODO + core.opSync("op_indexeddb_object_store_rename", { + databaseName: this[_store].database.name, + prevName: this[_name], + newName: name, + }); + this[_store].name = name; + this[_name] = name; } [_keyPath]; @@ -678,7 +1050,7 @@ context: "Argument 2", }); - // TODO + return addOrPut(this, value, key, false); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-add @@ -695,7 +1067,7 @@ context: "Argument 2", }); - // TODO + return addOrPut(this, value, key, true); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-delete diff --git a/ext/webstorage/02_indexeddb_types.d.ts b/ext/webstorage/02_indexeddb_types.d.ts index 95d4513caadae5..cf096c2c7b2ddb 100644 --- a/ext/webstorage/02_indexeddb_types.d.ts +++ b/ext/webstorage/02_indexeddb_types.d.ts @@ -9,3 +9,9 @@ interface Key { } type TransactionState = "active" | "inactive" | "committing" | "finished"; + +interface KeyGenerator { + current: number; + generateKey(): number; + possiblyUpdate(key: Key): void; +} diff --git a/ext/webstorage/indexeddb/idbtrait.rs b/ext/webstorage/indexeddb/idbtrait.rs index f4bcec65d73f79..a790d78e430d53 100644 --- a/ext/webstorage/indexeddb/idbtrait.rs +++ b/ext/webstorage/indexeddb/idbtrait.rs @@ -1,7 +1,16 @@ use async_trait::async_trait; +use deno_core::error::AnyError; +use deno_core::ZeroCopyBuf; #[async_trait] -trait IDB { - async fn open_database(&self, name: String, version: Option) -> (u64, u64); - async fn list_databases(&self) -> Vec; +pub trait IDB { + async fn open_database(&self, name: String, version: Option) -> Result<(u64, u64), AnyError>; + async fn list_databases(&self) -> Result, AnyError>; + + async fn object_store_rename(&self, database: String, store: String, new_name: String) -> Result<(), AnyError>; + async fn object_store_put(&self, database: String, store: String, value: ZeroCopyBuf, key: Option) -> Result<(), AnyError>; + async fn object_store_add(&self, database: String, store: String, value: ZeroCopyBuf, key: Option) -> Result<(), AnyError>; + async fn object_store_delete(&self, database: String, store: String, query: ) -> Result<(), AnyError>; + async fn object_store_clear(&self, database: String, store: String) -> Result<(), AnyError>; + async fn object_store_get(&self, database: String, store: String, query: ) -> Result<(), AnyError>; } diff --git a/ext/webstorage/indexeddb/mod.rs b/ext/webstorage/indexeddb/mod.rs index d7362b91c4e581..8a70ff8b4194ed 100644 --- a/ext/webstorage/indexeddb/mod.rs +++ b/ext/webstorage/indexeddb/mod.rs @@ -252,3 +252,33 @@ pub fn op_indexeddb_database_create_object_store( Ok(()) } + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ObjectStoreRenameArgs { + database_name: String, + prev_name: String, + new_name: String, +} + +// Ref: https://w3c.github.io/IndexedDB/#ref-for-dom-idbobjectstore-name%E2%91%A2 +pub fn op_indexeddb_object_store_rename( + state: &mut OpState, + args: ObjectStoreRenameArgs, + _: (), +) -> Result<(), AnyError> { + let conn = &state.borrow::().0; + + let mut stmt = conn.prepare_cached( + "UPDATE object_store_index SET name = ? WHERE name = ? AND database_name = ? RETURNING id", + )?; + let store_id: u64 = + stmt.query_row(params![args.new_name, args.prev_name, args.database_name], |row| row.get(0))?; + + let mut stmt = conn.prepare_cached( + "UPDATE object_store SET name = ? WHERE id = ?", + )?; + stmt.execute(params![args.new_name, store_id])?; + + Ok(()) +} diff --git a/ext/webstorage/indexeddb/sqlite_idb.rs b/ext/webstorage/indexeddb/sqlite_idb.rs index e69de29bb2d1d6..1417df09fed8c2 100644 --- a/ext/webstorage/indexeddb/sqlite_idb.rs +++ b/ext/webstorage/indexeddb/sqlite_idb.rs @@ -0,0 +1,17 @@ +use rusqlite::Connection; +use rusqlite::params; +use deno_core::error::AnyError; + +struct SqliteIDB(Connection); + +impl super::idbtrait::IDB for SqliteIDB { + async fn open_database(&self, name: String, version: Option) -> Result<(u64, u64), AnyError> { + todo!() + } + + async fn list_databases(&self) -> Result, AnyError> { + let mut stmt = self.0.prepare_cached("SELECT name FROM database")?; + let names = stmt.query(params![])?.map(|row| row.get(0).unwrap()).collect::>()?; + Ok(names) + } +} From fbdf3be6afeed9bb339415f86e70ee50b1ff16f6 Mon Sep 17 00:00:00 2001 From: crowlkats Date: Thu, 17 Mar 2022 19:54:52 +0100 Subject: [PATCH 07/14] objectstore.delete --- ext/webstorage/02_indexeddb.js | 50 ++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index 3bc056e17f48f8..845a9b6e3b61c1 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -177,6 +177,25 @@ } } + // 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"); // TODO: error + } else { + return createRange(null, null); + } + } + const key = valueToKey(value); + if (key === null) { + throw new DOMException("", "DataError"); // TODO: error + } + return createRange(key, key); + } + // Ref: https://w3c.github.io/IndexedDB/#compare-two-keys function compareTwoKeys(a, b) { const { type: ta, value: va } = a; @@ -862,11 +881,13 @@ // TODO: call op_indexeddb_database_create_object_store + const store = new Store(options.autoIncrement); + store.name = name; + store.keyPath = keypath; const objectStore = webidl.createBranded(IDBObjectStore); + objectStore[_name] = name; // TODO: objectstore name is inconsistent throughout the spec + objectStore[_store] = store; objectStore[_transaction] = this[_upgradeTransaction]; - objectStore[_name] = name; - objectStore[_keyPath] = keyPath; - objectStore[_autoIncrement] = options.autoIncrement; return objectStore; } @@ -1020,7 +1041,12 @@ ); } - const _autoIncrement = Symbol("[[autoIncrement]]"); + // Ref: https://w3c.github.io/IndexedDB/#delete-records-from-an-object-store + function deleteRecordsFromObjectStore(store, range) { + // TODO + return undefined; + } + const _keyPath = Symbol("[[keyPath]]"); const _store = Symbol("[[store]]"); // Ref: https://w3c.github.io/IndexedDB/#idbobjectstore @@ -1059,7 +1085,7 @@ throw new DOMException("", "TransactionInactiveError"); // TODO: error } - if (this[_name] === name) { + if (this[_store][_name] === name) { return; } @@ -1092,11 +1118,10 @@ return this[_transaction]; } - [_autoIncrement]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-autoincrement get autoIncrement() { webidl.assertBranded(this, IDBObjectStorePrototype); - return this[_autoIncrement]; + return this[_store].keyGenerator !== null; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-put @@ -1142,12 +1167,21 @@ prefix, context: "Argument 1", }); - // TODO + // TODO: 3. + 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); + // TODO } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-get From 3552ec6376718cf274f58352b1eec8d3eb4453b6 Mon Sep 17 00:00:00 2001 From: crowlkats Date: Thu, 17 Mar 2022 23:16:06 +0100 Subject: [PATCH 08/14] work --- ext/webstorage/02_indexeddb.js | 65 +++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index 845a9b6e3b61c1..f1c55e702ddfdc 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -1043,10 +1043,43 @@ // Ref: https://w3c.github.io/IndexedDB/#delete-records-from-an-object-store function deleteRecordsFromObjectStore(store, range) { - // TODO + // TODO: ops return undefined; } + // Ref: https://w3c.github.io/IndexedDB/#clear-an-object-store + function clearObjectStore(store) { + // TODO: ops + return undefined; + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-object-store + function retrieveValueFromObjectStore(store, range) { + // TODO: ops + if (res === undefined) { + return undefined; + } else { + return core.deserialize(res); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-values-from-an-object-store + function retrieveMultipleValuesFromObjectStore(store, range, count) { + // TODO: ops + core.opAsync("foo"); + return res.map((val) => core.deserialize(val)); + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-key-from-an-object-store + function retrieveKeyFromObjectStore(store, range) { + // TODO: ops + if (res === undefined) { + return undefined; + } else { + return keyToValue(res); + } + } + const _keyPath = Symbol("[[keyPath]]"); const _store = Symbol("[[store]]"); // Ref: https://w3c.github.io/IndexedDB/#idbobjectstore @@ -1181,7 +1214,14 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-clear clear() { webidl.assertBranded(this, IDBObjectStorePrototype); - // TODO + // TODO: 3. + 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 @@ -1193,7 +1233,12 @@ prefix, context: "Argument 1", }); - // TODO + // TODO: 3. + 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 @@ -1205,7 +1250,12 @@ prefix, context: "Argument 1", }); - // TODO + // TODO: 3. + 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 @@ -1223,7 +1273,12 @@ enforceRange: true, }); } - // TODO + // TODO: 3. + 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 From b42aaba08153a7e6c3d3a504f782c2c0465c240e Mon Sep 17 00:00:00 2001 From: crowlkats Date: Fri, 18 Mar 2022 22:38:02 +0100 Subject: [PATCH 09/14] work --- ext/webstorage/02_indexeddb.js | 565 ++++++++++++++++++++++++++++---- ext/webstorage/Cargo.toml | 2 +- ext/webstorage/indexeddb/mod.rs | 8 +- 3 files changed, 516 insertions(+), 59 deletions(-) diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index f1c55e702ddfdc..c575385d9916bc 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -184,14 +184,14 @@ } if (value === undefined || value === null) { if (nullDisallowed) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("", "DataError"); } else { return createRange(null, null); } } const key = valueToKey(value); if (key === null) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("", "DataError"); } return createRange(key, key); } @@ -347,7 +347,7 @@ request[_processed] = true; request[_done] = true; request[_result] = undefined; - request[_error] = new DOMException("", "AbortError"); // TODO: error + request[_error] = new DOMException("", "AbortError"); request.dispatchEvent( new Event("error", { bubbles: true, @@ -440,6 +440,7 @@ } source[_transaction][_requestList].push(request); + // TODO: use .then (async () => { // TODO: 5.1 let errored = false; @@ -902,16 +903,19 @@ }); if (this[_upgradeTransaction] === null) { - throw new DOMException("", "InvalidStateError"); // TODO: error message + throw new DOMException("", "InvalidStateError"); + message; } if (this[_upgradeTransaction][_state] !== "active") { - throw new DOMException("", "TransactionInactiveError"); // TODO: error message + throw new DOMException("", "TransactionInactiveError"); + message; } const store = MapPrototypeGet(this[_connection].objectStoreSet, name); if (store === undefined) { - throw new DOMException("", "NotFoundError"); // TODO: error message + throw new DOMException("", "NotFoundError"); + message; } MapPrototypeDelete(this[_connection].objectStoreSet, name); @@ -986,28 +990,28 @@ // TODO: 3. if (handle[_transaction][_state] !== "active") { - throw new DOMException("", "TransactionInactiveError"); // TODO: error + throw new DOMException("", "TransactionInactiveError"); } if (handle[_transaction][_mode] !== "readonly") { - throw new DOMException("", "ReadOnlyError"); // TODO: error + throw new DOMException("", "ReadOnlyError"); } if (handle[_store].keyPath !== null && key !== undefined) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("", "DataError"); } if ( handle[_store].keyPath === null && handle[_store].keyGenerator === null && key === undefined ) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("", "DataError"); } if (key !== undefined) { const r = valueToKey(key); if (r === null) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("", "DataError"); } key = r; } @@ -1019,16 +1023,16 @@ handle[_store].keyPath, ); if (kpk === null) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("", "DataError"); } if (kpk !== _failure) { key = kpk; } else { if (handle[_store].keyGenerator === null) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("", "DataError"); } else { if (!checkKeyCanBeInjectedIntoValue(cloned, handle[_store].keyPath)) { - throw new DOMException("", "DataError"); // TODO: error + throw new DOMException("", "DataError"); } } } @@ -1066,7 +1070,6 @@ // Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-values-from-an-object-store function retrieveMultipleValuesFromObjectStore(store, range, count) { // TODO: ops - core.opAsync("foo"); return res.map((val) => core.deserialize(val)); } @@ -1080,6 +1083,21 @@ } } + // Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-keys-from-an-object-store + function retrieveMultipleKeysFromObjectStore(store, range) { + // TODO: ops + return res.map((val) => keyToValue(val)); + } + + // Ref: https://w3c.github.io/IndexedDB/#count-the-records-in-a-range + function countRecordsInRange(storeOrIndex, range) { + // TODO: ops + } + + function iterateCursor(cursor, primaryKey, count = 1) { + // TODO + } + const _keyPath = Symbol("[[keyPath]]"); const _store = Symbol("[[store]]"); // Ref: https://w3c.github.io/IndexedDB/#idbobjectstore @@ -1111,11 +1129,11 @@ // TODO: 4. if (this[_transaction][_mode] !== "versionchange") { - throw new DOMException("", "InvalidStateError"); // TODO: error + throw new DOMException("", "InvalidStateError"); } if (this[_transaction][_state] !== "active") { - throw new DOMException("", "TransactionInactiveError"); // TODO: error + throw new DOMException("", "TransactionInactiveError"); } if (this[_store][_name] === name) { @@ -1208,7 +1226,10 @@ throw new DOMException("", "ReadOnlyError"); } const range = valueToKeyRange(query, true); - return asynchronouslyExecuteRequest(this, () => deleteRecordsFromObjectStore(this[_store], range)); + return asynchronouslyExecuteRequest( + this, + () => deleteRecordsFromObjectStore(this[_store], range), + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-clear @@ -1221,7 +1242,10 @@ if (this[_transaction][_mode] === "readonly") { throw new DOMException("", "ReadOnlyError"); } - return asynchronouslyExecuteRequest(this, () => clearObjectStore(this[_store])); + return asynchronouslyExecuteRequest( + this, + () => clearObjectStore(this[_store]), + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-get @@ -1238,7 +1262,10 @@ throw new DOMException("", "TransactionInactiveError"); } const range = valueToKeyRange(query, true); - return asynchronouslyExecuteRequest(this, () => retrieveValueFromObjectStore(this[_store], range)); + return asynchronouslyExecuteRequest( + this, + () => retrieveValueFromObjectStore(this[_store], range), + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-getkey @@ -1255,7 +1282,10 @@ throw new DOMException("", "TransactionInactiveError"); } const range = valueToKeyRange(query, true); - return asynchronouslyExecuteRequest(this, () => retrieveKeyFromObjectStore(this[_store], range)); + return asynchronouslyExecuteRequest( + this, + () => retrieveKeyFromObjectStore(this[_store], range), + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-getall @@ -1278,7 +1308,10 @@ throw new DOMException("", "TransactionInactiveError"); } const range = valueToKeyRange(query, true); - return asynchronouslyExecuteRequest(this, () => retrieveMultipleValuesFromObjectStore(this[_store], range, count)); + return asynchronouslyExecuteRequest( + this, + () => retrieveMultipleValuesFromObjectStore(this[_store], range, count), + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-getallkeys @@ -1296,7 +1329,15 @@ enforceRange: true, }); } - // TODO + // TODO: 3. + 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 @@ -1307,7 +1348,15 @@ prefix, context: "Argument 1", }); - // TODO + // TODO: 3. + 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 @@ -1322,7 +1371,24 @@ prefix, context: "Argument 2", }); - // TODO + // TODO: 3. + 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 @@ -1337,7 +1403,24 @@ prefix, context: "Argument 2", }); - // TODO + // TODO: 3. + 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 @@ -1349,11 +1432,15 @@ prefix, context: "Argument 1", }); - // TODO + // TODO: 3. + if (this[_transaction][_state] === "finished") { + throw new DOMException("", "InvalidStateError"); + } + // TODO: 5., 6. } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-createindex - createIndex(name, keypath, options = {}) { + createIndex(name, keyPath, options = {}) { webidl.assertBranded(this, IDBObjectStorePrototype); const prefix = "Failed to execute 'createIndex' on 'IDBObjectStore'"; webidl.requiredArguments(arguments.length, 2, { prefix }); @@ -1361,7 +1448,7 @@ prefix, context: "Argument 1", }); - keypath = webidl.converters["sequence or DOMString"](keypath, { + keyPath = webidl.converters["sequence or DOMString"](keyPath, { prefix, context: "Argument 2", }); @@ -1369,7 +1456,25 @@ prefix, context: "Argument 3", }); - // TODO + if (this[_transaction][_mode] !== "versionchange") { + throw new DOMException("", "InvalidStateError"); + } + // TODO: 4. + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + // TODO: 6. + if (!isValidKeyPath(keyPath)) { + throw new DOMException("", "SyntaxError"); + } + if (ArrayIsArray(keyPath) && options.multiEntry) { + throw new DOMException("", "InvalidAccessError"); + } + // TODO: 11. + // TODO: 12. + const indexHandle = webidl.createBranded(IDBIndex); + indexHandle[_storeHandle] = this; + return indexHandle; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-deleteindex @@ -1381,18 +1486,73 @@ prefix, context: "Argument 1", }); - // TODO + if (this[_transaction][_mode] !== "versionchange") { + throw new DOMException("", "InvalidStateError"); + } + // TODO: 4. + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + // TODO: 6., 7., 8. } } webidl.configurePrototype(IDBObjectStore); const IDBObjectStorePrototype = IDBObjectStore.prototype; + // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-referenced-value-from-an-index + function retrieveReferencedValueFromIndex(index, range) { + // TODO: ops + if (res === undefined) { + return undefined; + } else { + return core.deserialize(res); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-referenced-values-from-an-index + function retrieveMultipleReferencedValuesFromIndex(index, range, count) { + // TODO: ops + return res.map((val) => core.deserialize(val)); + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-index + function retrieveValueFromIndex(index, range) { + // TODO: ops + if (res === undefined) { + return undefined; + } else { + return keyToValue(res); + } + } + + // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-index + function retrieveMultipleValuesFromIndex(index, range, count) { + // TODO: ops + return res.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() { @@ -1407,31 +1567,43 @@ prefix: "Failed to set 'name' on 'IDBIndex'", context: "Argument 1", }); - // TODO + + if (this[_transaction][_mode] !== "versionchange") { + throw new DOMException("", "InvalidStateError"); + } + + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + + // TODO: 6., 7., 8. + + this[_index].name = name; + this[_name] = name; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-objectstore get objectStore() { webidl.assertBranded(this, IDBIndexPrototype); - // TODO + return this[_storeHandle]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-keypath get keyPath() { webidl.assertBranded(this, IDBIndexPrototype); - // TODO + return this[_storeHandle][_store].keyPath; // TODO: convert? } // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-multientry get multiEntry() { webidl.assertBranded(this, IDBIndexPrototype); - // TODO + return this[_index].multiEntry; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-unique get unique() { webidl.assertBranded(this, IDBIndexPrototype); - // TODO + return this[_index].unique; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-get @@ -1443,7 +1615,15 @@ prefix, context: "Argument 1", }); - // TODO + // TODO: 3. + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveReferencedValueFromIndex(this[_index], range), + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-getkey @@ -1455,7 +1635,15 @@ prefix, context: "Argument 1", }); - // TODO + // TODO: 3. + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveValueFromIndex(this[_index], range), + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-getall @@ -1473,7 +1661,16 @@ enforceRange: true, }); } - // TODO + // TODO: 3. + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => + retrieveMultipleReferencedValuesFromIndex(this[_index], range, count), + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-getallkeys @@ -1491,7 +1688,15 @@ enforceRange: true, }); } - // TODO + // TODO: 3. + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + const range = valueToKeyRange(query, true); + return asynchronouslyExecuteRequest( + this, + () => retrieveMultipleValuesFromIndex(this[_index], range, count), + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-count @@ -1502,7 +1707,15 @@ prefix, context: "Argument 1", }); - // TODO + // TODO: 3. + 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 @@ -1517,7 +1730,24 @@ prefix, context: "Argument 2", }); - // TODO + // TODO: 3. + 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 @@ -1532,7 +1762,24 @@ prefix, context: "Argument 2", }); - // TODO + // TODO: 3. + 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); @@ -1715,7 +1962,30 @@ 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 { @@ -1726,6 +1996,27 @@ /** @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) { + // TODO: the cursor’s object store position. + } + } + get [_effectiveKey]() { + if (this[_source] instanceof IDBObjectStore) { + return this[_position]; + } else if (this[_source] instanceof IDBIndex) { + // TODO: the cursor’s object store position. + } + } + [_source]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-source get source() { @@ -1741,16 +2032,17 @@ return this[_direction]; } + [_key]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-key get key() { webidl.assertBranded(this, IDBCursorPrototype); - // TODO + return keyToValue(this[_key]); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-primarykey get primaryKey() { webidl.assertBranded(this, IDBCursorPrototype); - // TODO + return keyToValue(this[_effectiveKey]); } [_request]; @@ -1774,9 +2066,21 @@ throw new TypeError("Count cannot be 0"); } if (this[_transaction][_state] !== "active") { - throw new DOMException("", "TransactionInactiveError"); // TODO: error + throw new DOMException("", "TransactionInactiveError"); } - // TODO + // TODO: 4. + if (!this[_gotValue]) { + throw new DOMException("", "InvalidStateError"); + } + this[_gotValue] = false; + this[_request][_processed] = false; + this[_request][_done] = false; + + return asynchronouslyExecuteRequest( + this, + () => iterateCursor(this, count), + this[_request], + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-continue @@ -1787,7 +2091,37 @@ prefix, context: "Argument 1", }); - // TODO + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + // TODO: 4. + 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][_processed] = false; + this[_request][_done] = false; + + return asynchronouslyExecuteRequest( + this, + () => iterateCursor(this, key), + this[_request], + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-continueprimarykey @@ -1803,7 +2137,62 @@ prefix, context: "Argument 2", }); - // TODO + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + // TODO: 3. + 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][_processed] = false; + this[_request][_done] = false; + + return asynchronouslyExecuteRequest( + this, + () => iterateCursor(this, key, primaryKey), + this[_request], + ); } // Ref: https://w3c.github.io/IndexedDB/#dom-idbcursor-update @@ -1815,13 +2204,67 @@ prefix, context: "Argument 1", }); - // TODO + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + if (this[_transaction][_mode] === "readonly") { + throw new DOMException("", "ReadOnlyError"); + } + // TODO: 4. + if (!this[_gotValue]) { + throw new DOMException("", "InvalidStateError"); + } + if (this[_keyOnly]) { + throw new DOMException("", "InvalidStateError"); + } + const cloned = clone(value); // TODO: during transaction? + 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); - // TODO + if (this[_transaction][_state] !== "active") { + throw new DOMException("", "TransactionInactiveError"); + } + if (this[_transaction][_mode] === "readonly") { + throw new DOMException("", "ReadOnlyError"); + } + // TODO: 4. + 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); @@ -1830,6 +2273,7 @@ const _requestList = Symbol("[[requestList]]"); const _state = Symbol("[[state]]"); const _mode = Symbol("[[mode]]"); + const _durabilityHint = Symbol("[[durabilityHint]]"); const _db = Symbol("[[db]]"); // Ref: https://w3c.github.io/IndexedDB/#idbtransaction class IDBTransaction extends EventTarget { @@ -1837,6 +2281,7 @@ /** @type {TransactionState} */ [_state] = "active"; [_mode]; + [_durabilityHint]; [_error]; [_db]; @@ -1860,7 +2305,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-durability get durability() { webidl.assertBranded(this, IDBTransactionPrototype); - // TODO + return this[_durabilityHint]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-db @@ -1884,19 +2329,29 @@ prefix, context: "Argument 1", }); - // TODO + if (this[_state] === "finished") { + throw new DOMException("", "InvalidStateError"); + } + // TODO: 2., 3. } // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-commit commit() { webidl.assertBranded(this, IDBTransactionPrototype); - // TODO + 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); - // TODO + if (this[_state] === "committing" || this[_state] === "finished") { + throw new DOMException("", "InvalidStateError"); + } + this[_state] = "inactive"; + abortTransaction(this, null); } } defineEventHandler(IDBTransaction.prototype, "abort"); diff --git a/ext/webstorage/Cargo.toml b/ext/webstorage/Cargo.toml index 027de5e4b918c2..6b5e7d5a73ac8e 100644 --- a/ext/webstorage/Cargo.toml +++ b/ext/webstorage/Cargo.toml @@ -17,6 +17,6 @@ path = "lib.rs" async-trait = "0.1.52" deno_core = { version = "0.124.0", path = "../../core" } deno_web = { version = "0.73.0", path = "../web" } +fallible-iterator = "0.2.0" rusqlite = { version = "0.25.3", features = ["unlock_notify", "bundled"] } serde = { version = "1.0.129", features = ["derive"] } -fallible-iterator = "0.2.0" diff --git a/ext/webstorage/indexeddb/mod.rs b/ext/webstorage/indexeddb/mod.rs index 38b001760d6a11..b7d4d2db763ef0 100644 --- a/ext/webstorage/indexeddb/mod.rs +++ b/ext/webstorage/indexeddb/mod.rs @@ -3,17 +3,17 @@ use super::DomExceptionNotSupportedError; use super::OriginStorageDir; +use crate::DomExceptionConstraintError; use deno_core::error::AnyError; use deno_core::op; use deno_core::OpState; use deno_core::Resource; +use fallible_iterator::FallibleIterator; use rusqlite::params; use rusqlite::Connection; use rusqlite::OptionalExtension; use serde::Deserialize; use std::borrow::Cow; -use fallible_iterator::FallibleIterator; -use crate::DomExceptionConstraintError; fn create_file_table(conn: &Connection) -> Result<(), AnyError> { let statements = r#" @@ -189,7 +189,9 @@ pub fn op_indexeddb_open( }) }) .optional()?; - let version = version.or_else(|| db.clone().map(|db| db.version)).unwrap_or(1); + let version = version + .or_else(|| db.clone().map(|db| db.version)) + .unwrap_or(1); let db = if let Some(db) = db { db From d2893e5c1aa9c1e42bbc4353c89039aa9982640b Mon Sep 17 00:00:00 2001 From: crowlkats Date: Sat, 19 Mar 2022 15:13:24 +0100 Subject: [PATCH 10/14] more work --- ext/webstorage/02_indexeddb.js | 224 +++++++++++++++++-------- ext/webstorage/indexeddb/idbtrait.rs | 41 ----- ext/webstorage/indexeddb/mod.rs | 3 - ext/webstorage/indexeddb/sqlite_idb.rs | 24 --- 4 files changed, 151 insertions(+), 141 deletions(-) delete mode 100644 ext/webstorage/indexeddb/idbtrait.rs delete mode 100644 ext/webstorage/indexeddb/sqlite_idb.rs diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index c575385d9916bc..b03ddf1db9a1b5 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -59,7 +59,9 @@ [ { key: "keyPath", - converter: webidl.converters["sequence or DOMString"], // TODO: nullable + converter: webidl.createNullableConverter( + webidl.converters["sequence or DOMString"], + ), defaultValue: null, }, { @@ -291,7 +293,6 @@ } // Ref: https://w3c.github.io/IndexedDB/#check-that-a-key-could-be-injected-into-a-value - // TODO: check function checkKeyCanBeInjectedIntoValue(value, keyPath) { const identifiers = keyPath.split("."); assert(identifiers.length !== 0); @@ -333,6 +334,7 @@ // TODO: 6. } + // Ref: https://w3c.github.io/IndexedDB/#abort-a-transaction function abortTransaction(transaction, error) { // TODO: 1. if (transaction[_mode] === "versionchange") { @@ -368,6 +370,7 @@ } } + // Ref: https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction function abortUpgradeTransaction(transaction) { // TODO } @@ -387,13 +390,11 @@ if (ArrayIsArray(keyPath)) { const result = []; for (let i = 0; i < keyPath.length; i++) { - const key = evaluateKeyPathOnValue(keyPath[i], value); // TODO: seems spec could be wrong and arguments should be swapped - // TODO: 2. + const key = evaluateKeyPathOnValue(value, keyPath[i]); // spec is wrong, arguments are reversed. if (key === _failure) { return _failure; } result[i] = key; - // TODO: 6. } return result; } @@ -427,7 +428,6 @@ } } } - // TODO: 5.. return value; } @@ -514,8 +514,25 @@ return request; } + // Ref: https://w3c.github.io/IndexedDB/#commit-a-transaction function commitTransaction(transaction) { - // TODO + transaction[_state] = "committing"; + (async () => { + // TODO: 2.1. + if (transaction[_state] !== "committing") { + return; + } + // TODO: 2.3., 2.4. + + if (transaction[_mode] === "versionchange") { + // TODO: 2.5.1. + } + transaction[_state] = "finished"; + transaction.dispatchEvent(new Event("complete")); + if (transaction[_mode] === "versionchange") { + transaction[_request][_transaction] = null; + } + })(); } const _result = Symbol("[[result]]"); @@ -535,19 +552,24 @@ [_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"); // TODO + throw new DOMException("", "InvalidStateError"); + } + if (this[_error]) { + return undefined; + } else { + return this[_result]; } - return this[_result]; // TODO: or undefined if the request resulted in an error } [_error] = null; get error() { webidl.assertBranded(this, IDBRequestPrototype); if (!this[_done]) { - throw new DOMException("", "InvalidStateError"); // TODO + throw new DOMException("", "InvalidStateError"); } return this[_error]; } @@ -587,6 +609,16 @@ webidl.configurePrototype(IDBOpenDBRequest); + // Ref: https://w3c.github.io/IndexedDB/#open-a-database + function openDatabase(name, version) { + // TODO + } + + // Ref: https://w3c.github.io/IndexedDB/#delete-a-database + function deleteDatabase(name, request) { + // TODO + } + // Ref: https://w3c.github.io/IndexedDB/#idbfactory class IDBFactory { constructor() { @@ -611,66 +643,29 @@ } if (version === 0) { - throw new TypeError(); // TODO + throw new TypeError(); } const request = webidl.createBranded(IDBOpenDBRequest); - try { - const [newVersion, dbVersion] = core.opSync( - "op_indexeddb_open", - name, - version, - ); - const connection = webidl.createBranded(IDBDatabase); - connection[_name] = name; - // TODO: connection[_version] = newVersion; - if (dbVersion < newVersion) { - for (const conn of connections.values()) { - if (!conn[_closePending]) { - conn.dispatchEvent( - new IDBVersionChangeEvent("versionchange", { - bubbles: false, - cancelable: false, - oldVersion: dbVersion, - newVersion, - }), - ); - } - } - // TODO: why should connections close? - for (const conn of connections.values()) { - if (!conn[_closePending]) { - request.dispatchEvent( - new IDBVersionChangeEvent("blocked", { - bubbles: false, - cancelable: false, - oldVersion: dbVersion, - newVersion, - }), - ); - break; - } - } - // Ref: https://w3c.github.io/IndexedDB/#upgrade-transaction-steps - // TODO: Wait until all connections in openConnections are closed. - const transaction = ""; // TODO - // TODO + (async () => { + try { + const res = openDatabase(name, version); + 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, + }), + ); } - request[_result] = connection; - 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; } @@ -687,7 +682,34 @@ const request = webidl.createBranded(IDBOpenDBRequest); - // TODO + (async () => { + try { + const res = deleteDatabase(name, request); + request[_processed] = true; + request[_result] = undefined; + request[_done] = true; + request.dispatchEvent( + new IDBVersionChangeEvent("success", { + bubbles: false, + cancelable: false, + oldVersion: res, + newVersion: null, + }), + ); + } catch (e) { + request[_processed] = true; + 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 @@ -830,7 +852,25 @@ context: "Argument 3", }); - // TODO + if (this[_closePending]) { + throw new DOMException("", "InvalidStateError"); + } + const scope = new Set( + ArrayIsArray(storeNames) ? storeNames : [storeNames], + ); + // TODO: 4. + if (scope.size === 0) { + throw new DOMException("", "InvalidAccessError"); + } + if (mode !== "readonly" && mode !== "readwrite") { + throw new TypeError(""); + } + const transaction = webidl.createBranded(IDBTransaction); + // TODO: connection + transaction[_mode] = mode; + transaction[_durabilityHint] = options.durability; + // TODO: scope + return transaction; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-close @@ -870,14 +910,14 @@ const keyPath = options.keyPath ?? null; if (options.keyPath !== null && !isValidKeyPath(options.keyPath)) { - throw new DOMException("", "SyntaxError"); // TODO + throw new DOMException("", "SyntaxError"); } if ( (typeof options.keyPath === "string" && options.keyPath.length === 0) || ArrayIsArray(options.keyPath) ) { - throw new DOMException("", "InvalidAccessError"); // TODO + throw new DOMException("", "InvalidAccessError"); } // TODO: call op_indexeddb_database_create_object_store @@ -950,7 +990,7 @@ // Ref: https://w3c.github.io/IndexedDB/#generate-a-key generateKey() { if (this.current > 9007199254740992) { - throw new DOMException("", "ConstraintError"); // TODO + throw new DOMException("", "ConstraintError"); } return this.current++; }, @@ -970,6 +1010,7 @@ } } + // 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) { @@ -1094,8 +1135,40 @@ // TODO: ops } - function iterateCursor(cursor, primaryKey, count = 1) { - // TODO + // 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"), + ); + } + // TODO: 4. + let position = cursor[_position]; + let objectStorePosition = cursor[_objectStorePosition]; + for (; count > 0; count--) { + // TODO: 9.1. + if (res === undefined) { + if (cursor[_source] instanceof IDBIndex) { + cursor[_objectStorePosition] = undefined; + } + if (!cursor[_keyOnly]) { + cursor[_value] = undefined; + } + return null; + } + // TODO: 9.3., 9.4. + } + cursor[_position] = position; + if (cursor[_source] instanceof IDBIndex) { + cursor[_objectStorePosition] = objectStorePosition; + } + // TODO: 12. + if (!cursor[_keyOnly]) { + // TODO: 13.1., 13.2. + } + cursor[_gotValue] = true; + return cursor; } const _keyPath = Symbol("[[keyPath]]"); @@ -1153,7 +1226,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-keypath get keyPath() { webidl.assertBranded(this, IDBObjectStorePrototype); - // TODO + return this[_keyPath]; // TODO: convert? } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-indexnames @@ -1472,7 +1545,12 @@ } // TODO: 11. // TODO: 12. + const index = new Index(); + index.name = name; + index.multiEntry = options.multiEntry; + index.unique = options.unique; const indexHandle = webidl.createBranded(IDBIndex); + indexHandle[_index] = index; indexHandle[_storeHandle] = this; return indexHandle; } diff --git a/ext/webstorage/indexeddb/idbtrait.rs b/ext/webstorage/indexeddb/idbtrait.rs deleted file mode 100644 index 398d231f24df98..00000000000000 --- a/ext/webstorage/indexeddb/idbtrait.rs +++ /dev/null @@ -1,41 +0,0 @@ -use async_trait::async_trait; -use deno_core::error::AnyError; -use deno_core::ZeroCopyBuf; - -#[async_trait] -pub trait IDB { - async fn open_database( - &self, - name: String, - version: Option, - ) -> Result<(u64, u64), AnyError>; - async fn list_databases(&self) -> Result, AnyError>; - - async fn object_store_rename( - &self, - database: String, - store: String, - new_name: String, - ) -> Result<(), AnyError>; - async fn object_store_put( - &self, - database: String, - store: String, - value: ZeroCopyBuf, - key: Option, - ) -> Result<(), AnyError>; - async fn object_store_add( - &self, - database: String, - store: String, - value: ZeroCopyBuf, - key: Option, - ) -> Result<(), AnyError>; - //async fn object_store_delete(&self, database: String, store: String, query: ) -> Result<(), AnyError>; - async fn object_store_clear( - &self, - database: String, - store: String, - ) -> Result<(), AnyError>; - //async fn object_store_get(&self, database: String, store: String, query: ) -> Result<(), AnyError>; -} diff --git a/ext/webstorage/indexeddb/mod.rs b/ext/webstorage/indexeddb/mod.rs index b7d4d2db763ef0..82d21cf097a214 100644 --- a/ext/webstorage/indexeddb/mod.rs +++ b/ext/webstorage/indexeddb/mod.rs @@ -1,6 +1,3 @@ -//mod idbtrait; -//mod sqlite_idb; - use super::DomExceptionNotSupportedError; use super::OriginStorageDir; use crate::DomExceptionConstraintError; diff --git a/ext/webstorage/indexeddb/sqlite_idb.rs b/ext/webstorage/indexeddb/sqlite_idb.rs deleted file mode 100644 index c52913c31be0a9..00000000000000 --- a/ext/webstorage/indexeddb/sqlite_idb.rs +++ /dev/null @@ -1,24 +0,0 @@ -use deno_core::error::AnyError; -use rusqlite::params; -use rusqlite::Connection; - -struct SqliteIDB(Connection); - -impl super::idbtrait::IDB for SqliteIDB { - async fn open_database( - &self, - name: String, - version: Option, - ) -> Result<(u64, u64), AnyError> { - todo!() - } - - async fn list_databases(&self) -> Result, AnyError> { - let mut stmt = self.0.prepare_cached("SELECT name FROM database")?; - let names = stmt - .query(params![])? - .map(|row| row.get(0).unwrap()) - .collect::>()?; - Ok(names) - } -} From 1b00304157dba61269c5384dcad49ef5c0a8b03b Mon Sep 17 00:00:00 2001 From: crowlkats Date: Thu, 24 Mar 2022 16:55:29 +0100 Subject: [PATCH 11/14] work --- ext/webstorage/02_indexeddb.js | 332 ++++++++---- ext/webstorage/indexeddb/mod.rs | 870 +++++++++++++++++++++++++------- 2 files changed, 914 insertions(+), 288 deletions(-) diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index b03ddf1db9a1b5..e38a8537dfbd3d 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -8,6 +8,7 @@ const { DOMException } = window.__bootstrap.domException; const { defineEventHandler, _canceledFlag } = window.__bootstrap.event; const { assert } = window.__bootstrap.infra; + const { Deferred } = window.__bootstrap.streams; const { NumberIsNaN, ArrayIsArray, @@ -22,6 +23,7 @@ SetPrototypeHas, SetPrototypeAdd, MathMin, + MathFloor, MapPrototypeKeys, } = window.__bootstrap.primordials; @@ -216,10 +218,14 @@ return 1; } else if (tb === "string") { return -1; + } else if (ta === "number") { + return 1; + } else if (tb === "number") { + return -1; } else if (ta === "date") { - assert(tb === "date"); return 1; } else { + assert(tb === "date"); return -1; } } @@ -236,16 +242,22 @@ } } case "string": { - if (va > vb) { - return 1; - } else if (va < vb) { + if (va < vb) { return -1; + } else if (vb < va) { + return 1; } else { return 0; } } case "binary": { - // TODO + if (va < vb) { + return -1; + } else if (vb < va) { + return -1; + } else { + return 0; + } } case "array": { const len = MathMin(va.length, vb.length); @@ -287,8 +299,8 @@ function isValidKeyPath(key) { if (typeof key === "string" && key.length === 0) { return true; - } else if (false) { - // TODO + } else { + // TODO: complete implementation } } @@ -329,14 +341,14 @@ function clone(transaction, value) { assert(transaction[_state] === "active"); transaction[_state] = "inactive"; - // TODO: 4. + // TODO: 3., 4.: what is StructuredSerializeForStorage? Do we have it? transaction[_state] = "active"; // TODO: 6. } // Ref: https://w3c.github.io/IndexedDB/#abort-a-transaction function abortTransaction(transaction, error) { - // TODO: 1. + // TODO: 1.:refactors ops to use sqlite transactions and use a resource if (transaction[_mode] === "versionchange") { abortUpgradeTransaction(transaction); } @@ -358,7 +370,7 @@ ); } if (transaction[_mode] === "versionchange") { - // TODO: 6.1. + // TODO: 6.1.: figure out connection & database structures } transaction.dispatchEvent( new Event("abort", { @@ -366,13 +378,13 @@ }), ); if (transaction[_mode] === "versionchange") { - // TODO: 6.3. + // TODO: 6.3.: the transaction should have an openrequest, but the spec doesnt specify this ever } } // Ref: https://w3c.github.io/IndexedDB/#abort-an-upgrade-transaction function abortUpgradeTransaction(transaction) { - // TODO + // TODO: figure out connection & database structures } const _failure = Symbol("failure"); @@ -453,7 +465,7 @@ return; } else { result = e; - // TODO: revert changes + // TODO: revert changes made by operation errored = true; } } @@ -468,7 +480,7 @@ request[_error] = result; // Ref: https://w3c.github.io/IndexedDB/#fire-an-error-event - // TODO: legacyOutputDidListenersThrowFlag? + // TODO(@crowlKats): support legacyOutputDidListenersThrowFlag const event = new Event("error", { bubbles: true, cancelable: true, @@ -479,7 +491,6 @@ request.dispatchEvent(event); if (request[_transaction][_state] === "active") { request[_transaction][_state] = "inactive"; - // TODO: 8.2. if (!event[_canceledFlag]) { abortTransaction(request[_transaction], request[_error]); return; @@ -493,18 +504,17 @@ 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, }); - // TODO: legacyOutputDidListenersThrowFlag? if (request[_transaction][_state] === "inactive") { request[_transaction][_state] = "active"; } request.dispatchEvent(event); if (request[_transaction][_state] === "active") { request[_transaction][_state] = "inactive"; - // TODO: 8.2. if (request[_transaction][_requestList].length === 0) { commitTransaction(request[_transaction]); } @@ -515,17 +525,20 @@ } // Ref: https://w3c.github.io/IndexedDB/#commit-a-transaction + // TODO: this is all very weird, and not sure how integrates with sqlite transactions function commitTransaction(transaction) { transaction[_state] = "committing"; (async () => { - // TODO: 2.1. + for (const request of transaction[_requestList]) { + await request[_processedDeferred].promise; + } if (transaction[_state] !== "committing") { return; } // TODO: 2.3., 2.4. if (transaction[_mode] === "versionchange") { - // TODO: 2.5.1. + // TODO: 2.5.1.: figure out connection & database structures } transaction[_state] = "finished"; transaction.dispatchEvent(new Event("complete")); @@ -540,6 +553,7 @@ const _source = Symbol("[[source]]"); const _transaction = Symbol("[[transaction]]"); const _processed = Symbol("[[processed]]"); + const _processedDeferred = Symbol("[[processedDeferred]]"); const _done = Symbol("[[done]]"); // Ref: https://w3c.github.io/IndexedDB/#idbrequest class IDBRequest extends EventTarget { @@ -548,6 +562,7 @@ webidl.illegalConstructor(); } + [_processedDeferred] = new Deferred(); [_processed]; [_done] = false; @@ -611,12 +626,12 @@ // Ref: https://w3c.github.io/IndexedDB/#open-a-database function openDatabase(name, version) { - // TODO + // TODO: figure out connection & database structures } // Ref: https://w3c.github.io/IndexedDB/#delete-a-database function deleteDatabase(name, request) { - // TODO + // TODO: figure out connection & database structures } // Ref: https://w3c.github.io/IndexedDB/#idbfactory @@ -772,11 +787,11 @@ close(forced) { this.closePending = true; if (forced) { - // TODO: 2 + // TODO: 2: somehow get all transactions } - // TODO: 3. + // TODO: 3.: somehow get all transactions if (forced) { - // TODO: 4. + // TODO: 4.: where is this event listened from? makes no sense } } } @@ -791,7 +806,7 @@ const _upgradeTransaction = Symbol("[[upgradeTransaction]]"); const _connection = Symbol("[[connection]]"); // Ref: https://w3c.github.io/IndexedDB/#idbdatabase - // TODO: finalizationRegistry + // TODO: finalizationRegistry: If an IDBDatabase object is garbage collected, the associated connection must be closed. class IDBDatabase extends EventTarget { /** @type {boolean} */ [_closePending] = false; @@ -858,7 +873,7 @@ const scope = new Set( ArrayIsArray(storeNames) ? storeNames : [storeNames], ); - // TODO: 4. + // TODO: 4.: should this be an op? should the names be cached? if (scope.size === 0) { throw new DOMException("", "InvalidAccessError"); } @@ -866,10 +881,10 @@ throw new TypeError(""); } const transaction = webidl.createBranded(IDBTransaction); - // TODO: connection + // TODO: connection: figure out connection & database structures transaction[_mode] = mode; transaction[_durabilityHint] = options.durability; - // TODO: scope + // TODO: scope: figure out connection & database structures return transaction; } @@ -914,16 +929,24 @@ } if ( - (typeof options.keyPath === "string" && options.keyPath.length === 0) || - ArrayIsArray(options.keyPath) + options.autoIncrement && + ((typeof options.keyPath === "string" && + options.keyPath.length === 0) || + ArrayIsArray(options.keyPath)) ) { throw new DOMException("", "InvalidAccessError"); } - // TODO: call op_indexeddb_database_create_object_store + core.opSync( + "op_indexeddb_database_create_object_store", + this[_name], + name, + // TODO: keypath: probably an enum, since it can be a string or array of strings and there is different behaviour depending on type + ); const store = new Store(options.autoIncrement); store.name = name; + store.database = this; store.keyPath = keypath; const objectStore = webidl.createBranded(IDBObjectStore); objectStore[_name] = name; // TODO: objectstore name is inconsistent throughout the spec @@ -944,22 +967,19 @@ if (this[_upgradeTransaction] === null) { throw new DOMException("", "InvalidStateError"); - message; } if (this[_upgradeTransaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); - message; } const store = MapPrototypeGet(this[_connection].objectStoreSet, name); if (store === undefined) { throw new DOMException("", "NotFoundError"); - message; } MapPrototypeDelete(this[_connection].objectStoreSet, name); - // TODO 6. + // TODO 6.: ops } } defineEventHandler(IDBDatabase.prototype, "abort"); @@ -973,11 +993,10 @@ class Store { /** @type {string} */ name; + /** @type {IDBDatabase} */ + database; - /** @type {Database} */ - database; // TODO - - keyPath; // TODO + keyPath; // TODO: should this be here? or somewhere else? /** @type {null | KeyGenerator} */ keyGenerator = null; @@ -992,15 +1011,17 @@ if (this.current > 9007199254740992) { throw new DOMException("", "ConstraintError"); } - return this.current++; + 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 = MathMin(key.value, 9007199254740992); - // TODO: 4. + const value = MathFloor(MathMin(key.value, 9007199254740992)); if (value >= this.current) { this.current = value + 1; } @@ -1023,12 +1044,40 @@ } } - // TODO: probably the rest should be an op. + 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; } // Ref: https://w3c.github.io/IndexedDB/#add-or-put function addOrPut(handle, value, key, noOverwrite) { - // TODO: 3. + // TODO: 3.: source has been deleted if (handle[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); @@ -1088,51 +1137,97 @@ // Ref: https://w3c.github.io/IndexedDB/#delete-records-from-an-object-store function deleteRecordsFromObjectStore(store, range) { - // TODO: ops + 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) { - // TODO: ops + core.opSync( + "op_indexeddb_object_store_clear", + store.database.name, + store.name, + ); return undefined; } - // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-object-store + // Ref: https://w3c.github.io/IndexexdDB/#retrieve-a-value-from-an-object-store function retrieveValueFromObjectStore(store, range) { - // TODO: ops - if (res === undefined) { + 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(res); + return core.deserialize(val); } } // Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-values-from-an-object-store function retrieveMultipleValuesFromObjectStore(store, range, count) { - // TODO: ops - return res.map((val) => core.deserialize(val)); + 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) { - // TODO: ops - if (res === undefined) { + const val = core.opSync( + "op_indexeddb_object_store_retrieve_key", + store.database.name, + store.name, + range, + ); + if (val === null) { return undefined; } else { - return keyToValue(res); + return keyToValue(val); } } // Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-keys-from-an-object-store - function retrieveMultipleKeysFromObjectStore(store, range) { - // TODO: ops - return res.map((val) => keyToValue(val)); + 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) { - // TODO: ops + 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 @@ -1143,7 +1238,7 @@ (cursor[_direction] === "next" || cursor[_direction] === "prev"), ); } - // TODO: 4. + // TODO: 4.: this and following tODOs are to do with ops let position = cursor[_position]; let objectStorePosition = cursor[_objectStorePosition]; for (; count > 0; count--) { @@ -1199,7 +1294,7 @@ context: "Argument 1", }); - // TODO: 4. + // TODO: 4.: source has been deleted if (this[_transaction][_mode] !== "versionchange") { throw new DOMException("", "InvalidStateError"); @@ -1213,11 +1308,12 @@ return; } - core.opSync("op_indexeddb_object_store_rename", { - databaseName: this[_store].database.name, - prevName: this[_name], - newName: name, - }); + core.opSync( + "op_indexeddb_object_store_rename", + this[_store].database.name, + this[_name], + name, + ); this[_store].name = name; this[_name] = name; } @@ -1232,7 +1328,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-indexnames get indexNames() { webidl.assertBranded(this, IDBObjectStorePrototype); - // TODO + // TODO: op maybe? or caching? } [_transaction]; @@ -1291,7 +1387,7 @@ prefix, context: "Argument 1", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1308,7 +1404,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-clear clear() { webidl.assertBranded(this, IDBObjectStorePrototype); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1330,7 +1426,7 @@ prefix, context: "Argument 1", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1350,7 +1446,7 @@ prefix, context: "Argument 1", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1376,7 +1472,7 @@ enforceRange: true, }); } - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1402,7 +1498,7 @@ enforceRange: true, }); } - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1421,7 +1517,7 @@ prefix, context: "Argument 1", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1444,7 +1540,7 @@ prefix, context: "Argument 2", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1476,7 +1572,7 @@ prefix, context: "Argument 2", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1505,11 +1601,11 @@ prefix, context: "Argument 1", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] === "finished") { throw new DOMException("", "InvalidStateError"); } - // TODO: 5., 6. + // TODO: 5., 6.: op? or cache? } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-createindex @@ -1532,19 +1628,19 @@ if (this[_transaction][_mode] !== "versionchange") { throw new DOMException("", "InvalidStateError"); } - // TODO: 4. + // TODO: 4.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 6. + // TODO: 6.: op? or cache? if (!isValidKeyPath(keyPath)) { throw new DOMException("", "SyntaxError"); } if (ArrayIsArray(keyPath) && options.multiEntry) { throw new DOMException("", "InvalidAccessError"); } - // TODO: 11. - // TODO: 12. + // TODO: 11.: ops + // TODO: 12.: seems we need a cache? const index = new Index(); index.name = name; index.multiEntry = options.multiEntry; @@ -1567,11 +1663,11 @@ if (this[_transaction][_mode] !== "versionchange") { throw new DOMException("", "InvalidStateError"); } - // TODO: 4. + // TODO: 4.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 6., 7., 8. + // TODO: 6., 7., 8.: op? } } webidl.configurePrototype(IDBObjectStore); @@ -1579,34 +1675,46 @@ // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-referenced-value-from-an-index function retrieveReferencedValueFromIndex(index, range) { - // TODO: ops - if (res === undefined) { + const val = core.opSync( + "op_indexeddb_index_retrieve_value", + // TODO: args (index needs to be structured properly) + ); + if (val === null) { return undefined; } else { - return core.deserialize(res); + return core.deserialize(val); } } // Ref: https://w3c.github.io/IndexedDB/#retrieve-multiple-referenced-values-from-an-index function retrieveMultipleReferencedValuesFromIndex(index, range, count) { - // TODO: ops - return res.map((val) => core.deserialize(val)); + const vals = core.opSync( + "op_indexeddb_index_retrieve_multiple_values", + // TODO: args (index needs to be structured properly) + ); + return vals.map((val) => core.deserialize(val)); } // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-index function retrieveValueFromIndex(index, range) { - // TODO: ops - if (res === undefined) { + const val = core.opSync( + "op_indexeddb_index_retrieve_value", + // TODO: args (index needs to be structured properly) + ); + if (val === undefined) { return undefined; } else { - return keyToValue(res); + return keyToValue(val); } } // Ref: https://w3c.github.io/IndexedDB/#retrieve-a-value-from-an-index function retrieveMultipleValuesFromIndex(index, range, count) { - // TODO: ops - return res.map((val) => keyToValue(val)); + const vals = core.opSync( + "op_indexeddb_index_retrieve_multiple_values", + // TODO: args (index needs to be structured properly) + ); + return vals.map((val) => keyToValue(val)); } class Index { @@ -1654,7 +1762,9 @@ throw new DOMException("", "TransactionInactiveError"); } - // TODO: 6., 7., 8. + // TODO: 6.: source has been deleted + // TODO: 7.: should it be this's _name? or this's _index's name + // TODO: 8.: op? or cache? this[_index].name = name; this[_name] = name; @@ -1693,7 +1803,7 @@ prefix, context: "Argument 1", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1713,7 +1823,7 @@ prefix, context: "Argument 1", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1739,7 +1849,7 @@ enforceRange: true, }); } - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1766,7 +1876,7 @@ enforceRange: true, }); } - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1785,7 +1895,7 @@ prefix, context: "Argument 1", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1808,7 +1918,7 @@ prefix, context: "Argument 2", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1840,7 +1950,7 @@ prefix, context: "Argument 2", }); - // TODO: 3. + // TODO: 3.: source has been deleted if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -2084,14 +2194,14 @@ if (this[_source] instanceof IDBObjectStore) { return this[_position]; } else if (this[_source] instanceof IDBIndex) { - // TODO: the cursor’s object store position. + return this[_objectStorePosition]; } } get [_effectiveKey]() { if (this[_source] instanceof IDBObjectStore) { return this[_position]; } else if (this[_source] instanceof IDBIndex) { - // TODO: the cursor’s object store position. + return this[_objectStorePosition]; } } @@ -2146,7 +2256,7 @@ if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 4. + // TODO: 4.: source has been deleted if (!this[_gotValue]) { throw new DOMException("", "InvalidStateError"); } @@ -2172,7 +2282,7 @@ if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 4. + // TODO: 4.: source has been deleted if (key !== undefined) { key = valueToKey(key); if (key === null) { @@ -2218,7 +2328,7 @@ if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 3. + // TODO: 3.: source has been deleted if (!(this[_source] instanceof IDBIndex)) { throw new DOMException("", "InvalidAccessError"); } @@ -2288,7 +2398,7 @@ if (this[_transaction][_mode] === "readonly") { throw new DOMException("", "ReadOnlyError"); } - // TODO: 4. + // TODO: 4.: source has been deleted if (!this[_gotValue]) { throw new DOMException("", "InvalidStateError"); } @@ -2327,7 +2437,7 @@ if (this[_transaction][_mode] === "readonly") { throw new DOMException("", "ReadOnlyError"); } - // TODO: 4. + // TODO: 4.: source has been deleted if (!this[_gotValue]) { throw new DOMException("", "InvalidStateError"); } @@ -2371,7 +2481,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstorenames get objectStoreNames() { webidl.assertBranded(this, IDBTransactionPrototype); - // TODO + // TODO: from _db and cache? or op? } // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-mode @@ -2410,7 +2520,7 @@ if (this[_state] === "finished") { throw new DOMException("", "InvalidStateError"); } - // TODO: 2., 3. + // TODO: 2., 3.: cache? } // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-commit diff --git a/ext/webstorage/indexeddb/mod.rs b/ext/webstorage/indexeddb/mod.rs index 82d21cf097a214..6854c393083ddc 100644 --- a/ext/webstorage/indexeddb/mod.rs +++ b/ext/webstorage/indexeddb/mod.rs @@ -3,128 +3,19 @@ use super::OriginStorageDir; use crate::DomExceptionConstraintError; use deno_core::error::AnyError; use deno_core::op; +use deno_core::serde_json; use deno_core::OpState; -use deno_core::Resource; +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 std::borrow::Cow; - -fn create_file_table(conn: &Connection) -> Result<(), AnyError> { - let statements = r#" - CREATE TABLE IF NOT EXISTS file ( - id INTEGER PRIMARY KEY, - refcount INTEGER NOT NULL - ); - - CREATE TRIGGER object_data_insert_trigger - AFTER INSERT ON object_data - FOR EACH ROW - WHEN NEW.file_ids IS NOT NULL - BEGIN - SELECT update_refcount(NULL, NEW.file_ids); - - CREATE TRIGGER object_data_update_trigger - AFTER UPDATE OF file_ids ON object_data - FOR EACH ROW - WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL - BEGIN - SELECT update_refcount(OLD.file_ids, NEW.file_ids); - - CREATE TRIGGER object_data_delete_trigger - AFTER DELETE ON object_data - FOR EACH ROW WHEN OLD.file_ids IS NOT NULL - BEGIN - SELECT update_refcount(OLD.file_ids, NULL); - - CREATE TRIGGER file_update_trigger - AFTER UPDATE ON file - FOR EACH ROW WHEN NEW.refcount = 0 - BEGIN - DELETE FROM file WHERE id = OLD.id; - "#; - conn.execute_batch(statements)?; - Ok(()) -} - -fn create_table(conn: &Connection) -> Result<(), AnyError> { - let statements = r#" - CREATE TABLE database ( - name TEXT PRIMARY KEY, - version INTEGER NOT NULL DEFAULT 0 - ) WITHOUT ROWID; - - CREATE TABLE object_store ( - id INTEGER PRIMARY KEY, - auto_increment INTEGER NOT NULL DEFAULT 0, - name TEXT NOT NULL, - key_path TEXT - ); - - CREATE TABLE object_store_index ( - id INTEGER PRIMARY KEY, - object_store_id INTEGER NOT NULL, - database_name TEXT NOT NULL, - name TEXT NOT NULL, - key_path TEXT NOT NULL, - unique_index INTEGER NOT NULL, - multientry INTEGER NOT NULL, - FOREIGN KEY (object_store_id) - REFERENCES object_store(id) - FOREIGN KEY (database_name) - REFERENCES database(name) - ); - - CREATE TABLE object_data ( - object_store_id INTEGER NOT NULL, - key BLOB NOT NULL, - index_data_values BLOB DEFAULT NULL, - file_ids TEXT, - data BLOB NOT NULL, - PRIMARY KEY (object_store_id, key), - FOREIGN KEY (object_store_id) - REFERENCES object_store(id) - ) WITHOUT ROWID; - - CREATE TABLE index_data ( - index_id INTEGER NOT NULL, - value BLOB NOT NULL, - object_data_key BLOB NOT NULL, - object_store_id INTEGER NOT NULL, - value_locale BLOB, - PRIMARY KEY (index_id, value, object_data_key), - FOREIGN KEY (index_id) - REFERENCES object_store_index(id), - FOREIGN KEY (object_store_id, object_data_key) - REFERENCES object_data(object_store_id, key) - ) WITHOUT ROWID; - - CREATE INDEX index_data_value_locale_index - ON index_data (index_id, value_locale, object_data_key, value) - WHERE value_locale IS NOT NULL; - - CREATE TABLE unique_index_data ( - index_id INTEGER NOT NULL, - value BLOB NOT NULL, - object_store_id INTEGER NOT NULL, - object_data_key BLOB NOT NULL, - value_locale BLOB, - PRIMARY KEY (index_id, value), - FOREIGN KEY (index_id) - REFERENCES object_store_index(id), - FOREIGN KEY (object_store_id, object_data_key) - REFERENCES object_data(object_store_id, key) - ) WITHOUT ROWID; - - CREATE INDEX unique_index_data_value_locale_index - ON unique_index_data (index_id, value_locale, object_data_key, value) - WHERE value_locale IS NOT NULL - "#; - conn.execute_batch(statements)?; - Ok(()) -} +use serde::Serialize; +use std::cmp::Ordering; #[derive(Clone)] struct Database { @@ -132,27 +23,10 @@ struct Database { version: u64, } -struct IndexedDbConnection { - conn: Connection, - close_pending: bool, -} - pub struct IndexedDbManager(Connection); -pub struct IndexedDbResource(Connection); -impl Resource for IndexedDbResource { - fn name(&self) -> Cow { - "indexedDb".into() - } -} - -// Ref: https://w3c.github.io/IndexedDB/#open-a-database #[op] -pub fn op_indexeddb_open( - state: &mut OpState, - name: String, - version: Option, -) -> Result<(u64, u64), AnyError> { +pub fn op_indexeddb_open(state: &mut OpState) -> Result<(), AnyError> { if state.try_borrow::().is_none() { let path = state.try_borrow::().ok_or_else(|| { DomExceptionNotSupportedError::new( @@ -168,12 +42,84 @@ pub fn op_indexeddb_open( PRAGMA foreign_keys = ON; "; conn.execute_batch(initial_pragmas)?; - create_table(&conn)?; + + 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.put(IndexedDbManager(conn)); } + 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 = @@ -202,95 +148,665 @@ pub fn op_indexeddb_open( Ok((version, db.version)) } -// Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore +// 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)) + .map(|row| row.get(0)) .collect::>()?; Ok(names) } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateObjectStoreArgs { - database_name: String, - name: String, - key_path: Option, - auto_increment: bool, -} - // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore #[op] pub fn op_indexeddb_database_create_object_store( state: &mut OpState, - args: CreateObjectStoreArgs, - _: (), + database_name: String, + name: String, + key_path: Option, ) -> Result<(), AnyError> { let conn = &state.borrow::().0; - // TODO: this might be doable on the JS side let mut stmt = conn.prepare_cached( - "SELECT * FROM object_store_index WHERE name = ? AND database_name = ?", + "SELECT * FROM object_store WHERE name = ? AND database_name = ?", )?; - if stmt.exists(params![args.name, args.database_name])? { - return Err(DomExceptionConstraintError::new("").into()); // TODO + 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, keyPath) VALUES (?, ?) RETURNING id", + "INSERT INTO object_store (name, key_path, unique_index, database_name) VALUES (?, ?, ?, ?)", )?; - let store_id: u64 = - stmt.query_row(params![args.name, args.key_path], |row| row.get(0))?; - - let mut stmt = conn.prepare_cached("INSERT INTO object_store_index (object_store_id, database_name, name, key_path) VALUES (?, ?, ?, ?)")?; stmt.execute(params![ - store_id, - args.database_name, - args.name, - args.key_path - ])?; // TODO: more args needed + name, + key_path, + // TODO: unique_index + database_name, + ])?; Ok(()) } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ObjectStoreRenameArgs { +// 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/#ref-for-dom-idbobjectstore-name%E2%91%A2 +// 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: todo!(), + unique: bool, + multi_entry: bool, +} + +// Ref: https://w3c.github.io/IndexedDB/#delete-records-from-an-object-store #[op] -pub fn op_indexeddb_object_store_rename( +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, - args: ObjectStoreRenameArgs, - _: (), + database_name: String, + store_name: String, + range: Range, ) -> Result<(), AnyError> { let conn = &state.borrow::().0; let mut stmt = conn.prepare_cached( - "UPDATE object_store_index SET name = ? WHERE name = ? AND database_name = ? RETURNING id", + "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 store_id: u64 = stmt.query_row( - params![args.new_name, args.prev_name, args.database_name], - |row| row.get(0), + 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("UPDATE object_store SET name = ? WHERE id = ?")?; - stmt.execute(params![args.new_name, store_id])?; + 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(val.into()) + } else { + None + } + }) + .transpose() + }); + + Ok(res.count() as u64) +} + +// 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, +) -> 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) +} From 2757414da75d7d84d65bc38ddaf8a039c62295d0 Mon Sep 17 00:00:00 2001 From: crowlkats Date: Sat, 26 Mar 2022 19:32:03 +0100 Subject: [PATCH 12/14] work --- ext/webstorage/02_indexeddb.js | 124 +++++++++++++++++--------------- ext/webstorage/indexeddb/mod.rs | 33 +++++++++ 2 files changed, 99 insertions(+), 58 deletions(-) diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index e38a8537dfbd3d..1dfb981256f504 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -134,11 +134,6 @@ type: "string", value: input, }; - } else if (false) { // TODO: is a buffer source type - return { - type: "binary", - value: input.slice(), - }; } else if (ArrayIsArray(input)) { SetPrototypeAdd(seen, input); const keys = []; @@ -154,7 +149,15 @@ value: keys, }; } else { - return null; + try { + const value = webidl.converters.BufferSource(input); + return { + type: "binary", + value: value.slice(), + }; + } catch (_) { + return null; + } } } @@ -288,7 +291,7 @@ case "date": return new Date(key.value); case "binary": - return new Uint8Array(key.value).buffer; // TODO: check + return new Uint8Array(key.value).buffer; case "array": { return key.value.map(keyToValue); } @@ -341,14 +344,15 @@ function clone(transaction, value) { assert(transaction[_state] === "active"); transaction[_state] = "inactive"; - // TODO: 3., 4.: what is StructuredSerializeForStorage? Do we have it? + const serialized = core.serialize(value); // TODO: add bool for extra SAB handling + const cloned = core.deserialize(serialized); transaction[_state] = "active"; - // TODO: 6. + return cloned; } // Ref: https://w3c.github.io/IndexedDB/#abort-a-transaction function abortTransaction(transaction, error) { - // TODO: 1.:refactors ops to use sqlite transactions and use a resource + core.opSync("op_indexeddb_transaction_abort", transaction[_rid]); if (transaction[_mode] === "versionchange") { abortUpgradeTransaction(transaction); } @@ -358,7 +362,7 @@ } for (const request of transaction[_requestList]) { // TODO: abort the steps to asynchronously execute a request - request[_processed] = true; + request[_processedDeferred].resolve(); request[_done] = true; request[_result] = undefined; request[_error] = new DOMException("", "AbortError"); @@ -379,6 +383,7 @@ ); if (transaction[_mode] === "versionchange") { // TODO: 6.3.: the transaction should have an openrequest, but the spec doesnt specify this ever + // TODO: add a global map for openrequests and find by transaction } } @@ -469,7 +474,7 @@ errored = true; } } - request[_processed] = true; + request[_processedDeferred].resolve(); source[_transaction][_requestList].slice( source[_transaction][_requestList].findIndex((r) => r === request), 1, @@ -525,13 +530,13 @@ } // Ref: https://w3c.github.io/IndexedDB/#commit-a-transaction - // TODO: this is all very weird, and not sure how integrates with sqlite transactions 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; } @@ -552,7 +557,6 @@ const _error = Symbol("[[error]]"); const _source = Symbol("[[source]]"); const _transaction = Symbol("[[transaction]]"); - const _processed = Symbol("[[processed]]"); const _processedDeferred = Symbol("[[processedDeferred]]"); const _done = Symbol("[[done]]"); // Ref: https://w3c.github.io/IndexedDB/#idbrequest @@ -563,7 +567,6 @@ } [_processedDeferred] = new Deferred(); - [_processed]; [_done] = false; [_result]; @@ -700,7 +703,7 @@ (async () => { try { const res = deleteDatabase(name, request); - request[_processed] = true; + request[_processedDeferred].resolve(); request[_result] = undefined; request[_done] = true; request.dispatchEvent( @@ -712,7 +715,7 @@ }), ); } catch (e) { - request[_processed] = true; + request[_processedDeferred].resolve(); request[_error] = e; request[_done] = true; request.dispatchEvent( @@ -791,7 +794,7 @@ } // TODO: 3.: somehow get all transactions if (forced) { - // TODO: 4.: where is this event listened from? makes no sense + // TODO: 4.: fire to IDBDatabase class } } } @@ -880,8 +883,10 @@ if (mode !== "readonly" && mode !== "readwrite") { throw new TypeError(""); } + let rid = core.opSync("op_indexeddb_transaction_create"); const transaction = webidl.createBranded(IDBTransaction); // TODO: connection: figure out connection & database structures + transaction[_rid] = rid; transaction[_mode] = mode; transaction[_durabilityHint] = options.durability; // TODO: scope: figure out connection & database structures @@ -1077,7 +1082,7 @@ // Ref: https://w3c.github.io/IndexedDB/#add-or-put function addOrPut(handle, value, key, noOverwrite) { - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (handle[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); @@ -1294,7 +1299,7 @@ context: "Argument 1", }); - // TODO: 4.: source has been deleted + // TODO: 4.: source has been deleted: be an op if (this[_transaction][_mode] !== "versionchange") { throw new DOMException("", "InvalidStateError"); @@ -1322,13 +1327,13 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-keypath get keyPath() { webidl.assertBranded(this, IDBObjectStorePrototype); - return this[_keyPath]; // TODO: convert? + return this[_keyPath]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-indexnames get indexNames() { webidl.assertBranded(this, IDBObjectStorePrototype); - // TODO: op maybe? or caching? + // TODO: add indexset to this } [_transaction]; @@ -1387,7 +1392,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1404,7 +1409,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-clear clear() { webidl.assertBranded(this, IDBObjectStorePrototype); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1426,7 +1431,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1446,7 +1451,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1472,7 +1477,7 @@ enforceRange: true, }); } - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1498,7 +1503,7 @@ enforceRange: true, }); } - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1517,7 +1522,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1540,7 +1545,7 @@ prefix, context: "Argument 2", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1572,7 +1577,7 @@ prefix, context: "Argument 2", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1601,11 +1606,11 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] === "finished") { throw new DOMException("", "InvalidStateError"); } - // TODO: 5., 6.: op? or cache? + // TODO: check indexset on this } // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-createindex @@ -1628,11 +1633,11 @@ if (this[_transaction][_mode] !== "versionchange") { throw new DOMException("", "InvalidStateError"); } - // TODO: 4.: source has been deleted + // TODO: 4.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 6.: op? or cache? + // TODO: 6.: op if (!isValidKeyPath(keyPath)) { throw new DOMException("", "SyntaxError"); } @@ -1640,7 +1645,7 @@ throw new DOMException("", "InvalidAccessError"); } // TODO: 11.: ops - // TODO: 12.: seems we need a cache? + // TODO: 12.: indexset on this const index = new Index(); index.name = name; index.multiEntry = options.multiEntry; @@ -1663,11 +1668,11 @@ if (this[_transaction][_mode] !== "versionchange") { throw new DOMException("", "InvalidStateError"); } - // TODO: 4.: source has been deleted + // TODO: 4.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 6., 7., 8.: op? + // TODO: 6., 7., 8.: op } } webidl.configurePrototype(IDBObjectStore); @@ -1762,7 +1767,7 @@ throw new DOMException("", "TransactionInactiveError"); } - // TODO: 6.: source has been deleted + // TODO: 6.: source has been deleted: be an op // TODO: 7.: should it be this's _name? or this's _index's name // TODO: 8.: op? or cache? @@ -1779,7 +1784,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-keypath get keyPath() { webidl.assertBranded(this, IDBIndexPrototype); - return this[_storeHandle][_store].keyPath; // TODO: convert? + return this[_storeHandle][_store].keyPath; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbindex-multientry @@ -1803,7 +1808,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1823,7 +1828,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1849,7 +1854,7 @@ enforceRange: true, }); } - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1876,7 +1881,7 @@ enforceRange: true, }); } - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1895,7 +1900,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1918,7 +1923,7 @@ prefix, context: "Argument 2", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1950,7 +1955,7 @@ prefix, context: "Argument 2", }); - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -2256,12 +2261,12 @@ if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 4.: source has been deleted + // TODO: 4.: source has been deleted: be an op if (!this[_gotValue]) { throw new DOMException("", "InvalidStateError"); } this[_gotValue] = false; - this[_request][_processed] = false; + this[_request][_processedDeferred] = new Deferred(); this[_request][_done] = false; return asynchronouslyExecuteRequest( @@ -2282,7 +2287,7 @@ if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 4.: source has been deleted + // TODO: 4.: source has been deleted: be an op if (key !== undefined) { key = valueToKey(key); if (key === null) { @@ -2302,7 +2307,7 @@ } } this[_gotValue] = false; - this[_request][_processed] = false; + this[_request][_processedDeferred] = new Deferred(); this[_request][_done] = false; return asynchronouslyExecuteRequest( @@ -2328,7 +2333,7 @@ if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 3.: source has been deleted + // TODO: 3.: source has been deleted: be an op if (!(this[_source] instanceof IDBIndex)) { throw new DOMException("", "InvalidAccessError"); } @@ -2373,7 +2378,7 @@ throw new DOMException("", "DataError"); } this[_gotValue] = false; - this[_request][_processed] = false; + this[_request][_processedDeferred] = new Deferred(); this[_request][_done] = false; return asynchronouslyExecuteRequest( @@ -2398,14 +2403,14 @@ if (this[_transaction][_mode] === "readonly") { throw new DOMException("", "ReadOnlyError"); } - // TODO: 4.: source has been deleted + // TODO: 4.: source has been deleted: be an op if (!this[_gotValue]) { throw new DOMException("", "InvalidStateError"); } if (this[_keyOnly]) { throw new DOMException("", "InvalidStateError"); } - const cloned = clone(value); // TODO: during transaction? + const cloned = clone(value); // TODO: during transaction?: open issue or mail if (this[_effectiveObjectStore][_store].keyPath !== null) { const kpk = extractKeyFromValueUsingKeyPath( cloned, @@ -2437,7 +2442,7 @@ if (this[_transaction][_mode] === "readonly") { throw new DOMException("", "ReadOnlyError"); } - // TODO: 4.: source has been deleted + // TODO: 4.: source has been deleted: be an op if (!this[_gotValue]) { throw new DOMException("", "InvalidStateError"); } @@ -2463,8 +2468,11 @@ const _mode = Symbol("[[mode]]"); const _durabilityHint = Symbol("[[durabilityHint]]"); const _db = Symbol("[[db]]"); + const _rid = Symbol("[[rid]]"); // Ref: https://w3c.github.io/IndexedDB/#idbtransaction class IDBTransaction extends EventTarget { + [_rid]; + [_requestList] = []; /** @type {TransactionState} */ [_state] = "active"; @@ -2481,7 +2489,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstorenames get objectStoreNames() { webidl.assertBranded(this, IDBTransactionPrototype); - // TODO: from _db and cache? or op? + // TODO: from _db and cache } // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-mode @@ -2520,7 +2528,7 @@ if (this[_state] === "finished") { throw new DOMException("", "InvalidStateError"); } - // TODO: 2., 3.: cache? + // TODO: 2., 3.: cache } // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-commit diff --git a/ext/webstorage/indexeddb/mod.rs b/ext/webstorage/indexeddb/mod.rs index 6854c393083ddc..6aa12aa38f0fa8 100644 --- a/ext/webstorage/indexeddb/mod.rs +++ b/ext/webstorage/indexeddb/mod.rs @@ -5,6 +5,7 @@ use deno_core::error::AnyError; use deno_core::op; use deno_core::serde_json; use deno_core::OpState; +use deno_core::ResourceId; use deno_core::ZeroCopyBuf; use fallible_iterator::FallibleIterator; use rusqlite::params; @@ -113,6 +114,38 @@ pub fn op_indexeddb_open(state: &mut OpState) -> Result<(), AnyError> { Ok(()) } +pub struct Transaction(rusqlite::Transaction<'static>); + +#[op] +pub fn op_indexeddb_transaction_create( + state: &mut OpState, +) -> Result { + let idbmanager = state.borrow_mut::(); + let transaction = idbmanager.0.transaction()?; + let rid = state.resource_table.add(Transaction(transaction)); + Ok(rid) +} + +#[op] +pub fn op_indexeddb_transaction_commit( + state: &mut OpState, + rid: ResourceId, +) -> Result<(), AnyError> { + let rid = state.resource_table.take::(rid)?; + rid.0.commit()?; + Ok(()) +} + +#[op] +pub fn op_indexeddb_transaction_abort( + state: &mut OpState, + rid: ResourceId, +) -> Result<(), AnyError> { + let rid = state.resource_table.take::(rid)?; + rid.0.rollback()?; + Ok(()) +} + // Ref: https://w3c.github.io/IndexedDB/#open-a-database #[op] pub fn op_indexeddb_open_database( From d450979267dc1e8e4725dab5a8432d4ca87a193d Mon Sep 17 00:00:00 2001 From: crowlkats Date: Wed, 30 Mar 2022 01:24:41 +0200 Subject: [PATCH 13/14] work --- Cargo.lock | 1 + core/bindings.rs | 18 +- ext/webstorage/02_indexeddb.js | 530 +++++++++++++++++++++++++------- ext/webstorage/Cargo.toml | 2 +- ext/webstorage/indexeddb/mod.rs | 93 +++++- ext/webstorage/lib.rs | 28 ++ runtime/errors.rs | 1 + 7 files changed, 565 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf73ff0072032e..aa153b498ae620 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3347,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/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index 1dfb981256f504..6abb49ae0f1d8b 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -300,10 +300,16 @@ // Ref: https://w3c.github.io/IndexedDB/#valid-key-path function isValidKeyPath(key) { - if (typeof key === "string" && key.length === 0) { - return true; - } else { + if (typeof key === "string") { + if (key.length === 0) { + return true; + } else { + } // TODO: complete implementation + } else if (ArrayIsArray(key)) { + return key.every(isValidKeyPath); + } else { + return false; } } @@ -344,13 +350,19 @@ function clone(transaction, value) { assert(transaction[_state] === "active"); transaction[_state] = "inactive"; - const serialized = core.serialize(value); // TODO: add bool for extra SAB handling + 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") { @@ -374,7 +386,7 @@ ); } if (transaction[_mode] === "versionchange") { - // TODO: 6.1.: figure out connection & database structures + transaction[_connection][_upgradeTransaction] = null; } transaction.dispatchEvent( new Event("abort", { @@ -382,14 +394,14 @@ }), ); if (transaction[_mode] === "versionchange") { - // TODO: 6.3.: the transaction should have an openrequest, but the spec doesnt specify this ever + // 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: figure out connection & database structures + // TODO } const _failure = Symbol("failure"); @@ -543,7 +555,7 @@ // TODO: 2.3., 2.4. if (transaction[_mode] === "versionchange") { - // TODO: 2.5.1.: figure out connection & database structures + transaction[_connection][_upgradeTransaction] = null; } transaction[_state] = "finished"; transaction.dispatchEvent(new Event("complete")); @@ -627,14 +639,109 @@ 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[_db].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 - function openDatabase(name, version) { - // TODO: figure out connection & database structures + /** + * @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 - function deleteDatabase(name, request) { - // TODO: figure out connection & database structures + 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 @@ -668,7 +775,7 @@ (async () => { try { - const res = openDatabase(name, version); + const res = await openDatabase(name, version, request); request[_result] = res; request[_done] = true; request.dispatchEvent(new Event("success")); @@ -702,7 +809,7 @@ (async () => { try { - const res = deleteDatabase(name, request); + const res = await deleteDatabase(name, request); request[_processedDeferred].resolve(); request[_result] = undefined; request[_done] = true; @@ -774,51 +881,48 @@ // Ref: https://w3c.github.io/IndexedDB/#database-connection class Connection { - /** @type {Set} */ - databases = new Set(); + /** @type {IDBDatabase} */ + database; /** @type {number} */ version; /** @type {boolean} */ closePending = false; - /** @type {Map} */ - objectStoreSet; - /** - * @param forced {boolean} - */ - // Ref: https://w3c.github.io/IndexedDB/#close-a-database-connection - close(forced) { - this.closePending = true; - if (forced) { - // TODO: 2: somehow get all transactions - } - // TODO: 3.: somehow get all transactions - if (forced) { - // TODO: 4.: fire to IDBDatabase class - } - } + objectStoreSet; // TODO + } + + class Database { + /** @type {number} */ + version; + /** @type {string} */ + name; } - /** @type {Set} */ + /** @type {Set} */ const connections = new Set(); const _name = Symbol("[[name]]"); const _version = Symbol("[[version]]"); - const _closePending = Symbol("[[closePending]]"); const _objectStores = Symbol("[[objectStores]]"); const _upgradeTransaction = Symbol("[[upgradeTransaction]]"); - const _connection = Symbol("[[connection]]"); + const _db = Symbol("[[db]]"); + const _closePending = Symbol("[[closePending]]"); + const _objectStoreSet = Symbol("[[objectStoreSet]]"); + const _close = Symbol("[[close]]"); // 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 {boolean} */ - [_closePending] = false; /** @type {Set} */ [_objectStores] = new Set(); /** @type {(IDBTransaction | null)} */ [_upgradeTransaction] = null; - /** @type {Connection} */ - [_connection]; + /** @type {Database} */ + [_db]; + /** @type {boolean} */ + [_closePending] = false; + + /** @type {Map} */ + [_objectStoreSet]; // TODO: update on upgrade transaction constructor() { super(); @@ -844,7 +948,7 @@ webidl.assertBranded(this, IDBDatabasePrototype); return ArrayPrototypeSort([ ...new SafeArrayIterator( - MapPrototypeKeys(this[_connection].objectStoreSet), + MapPrototypeKeys(this[_objectStoreSet]), ), ]); } @@ -870,7 +974,7 @@ context: "Argument 3", }); - if (this[_closePending]) { + if (this[_connection][_closePending]) { throw new DOMException("", "InvalidStateError"); } const scope = new Set( @@ -883,20 +987,35 @@ if (mode !== "readonly" && mode !== "readwrite") { throw new TypeError(""); } - let rid = core.opSync("op_indexeddb_transaction_create"); + const rid = core.opSync("op_indexeddb_transaction_create"); const transaction = webidl.createBranded(IDBTransaction); - // TODO: connection: figure out connection & database structures + transaction[_connection] = this; transaction[_rid] = rid; transaction[_mode] = mode; transaction[_durabilityHint] = options.durability; - // TODO: scope: figure out connection & database structures + // TODO: scope: get all stores and filter keep only ones in scope & assign to transaction[_scope] return transaction; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-close close() { webidl.assertBranded(this, IDBDatabasePrototype); - this[_connection].close(false); + this[_close](false); + } + + /** + * @param forced {boolean} + */ + // Ref: https://w3c.github.io/IndexedDB/#close-a-database-connection + [_close](forced) { + this[_closePending] = true; + if (forced) { + // TODO: 2: somehow get all transactions + } + // TODO: 3.: somehow get all transactions + if (forced) { + this.dispatchEvent(new Event("close")); + } } // Ref: https://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore @@ -946,7 +1065,7 @@ "op_indexeddb_database_create_object_store", this[_name], name, - // TODO: keypath: probably an enum, since it can be a string or array of strings and there is different behaviour depending on type + keyPath, ); const store = new Store(options.autoIncrement); @@ -978,11 +1097,11 @@ throw new DOMException("", "TransactionInactiveError"); } - const store = MapPrototypeGet(this[_connection].objectStoreSet, name); + const store = MapPrototypeGet(this[_objectStoreSet], name); if (store === undefined) { throw new DOMException("", "NotFoundError"); } - MapPrototypeDelete(this[_connection].objectStoreSet, name); + MapPrototypeDelete(this[_objectStoreSet], name); // TODO 6.: ops } @@ -995,6 +1114,7 @@ webidl.configurePrototype(IDBDatabase); const IDBDatabasePrototype = IDBDatabase.prototype; + // Ref: https://w3c.github.io/IndexedDB/#object-store-construct class Store { /** @type {string} */ name; @@ -1080,9 +1200,51 @@ 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) { - // TODO: 3.: source has been deleted: be an op + assertObjectStore(handle); if (handle[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); @@ -1243,12 +1405,141 @@ (cursor[_direction] === "next" || cursor[_direction] === "prev"), ); } - // TODO: 4.: this and following tODOs are to do with ops + /** @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--) { - // TODO: 9.1. - if (res === undefined) { + 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; } @@ -1257,15 +1548,21 @@ } return null; } - // TODO: 9.3., 9.4. + + position = foundRecord[0]; + if (cursor[_source] instanceof IDBIndex) { + objectStorePosition = core.deserialize(foundRecord[1]); + } } + cursor[_position] = position; if (cursor[_source] instanceof IDBIndex) { cursor[_objectStorePosition] = objectStorePosition; } - // TODO: 12. + cursor[_key] = foundRecord[0]; if (!cursor[_keyOnly]) { - // TODO: 13.1., 13.2. + // TODO: referencedValue: investigate it, and replace normal value usages for it where appropriate. + cursor[_value] = core.deserialize(foundRecord[1]); } cursor[_gotValue] = true; return cursor; @@ -1273,6 +1570,7 @@ const _keyPath = Symbol("[[keyPath]]"); const _store = Symbol("[[store]]"); + const _indexSet = Symbol("[[indexSet]]"); // Ref: https://w3c.github.io/IndexedDB/#idbobjectstore class IDBObjectStore { constructor() { @@ -1283,7 +1581,10 @@ [_transaction]; /** @type {Store} */ [_store]; + /** @type {Map} */ + [_indexSet]; // TODO: set + /** @type {string} */ [_name]; // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-name get name() { @@ -1298,21 +1599,16 @@ prefix: "Failed to set 'name' on 'IDBObjectStore'", context: "Argument 1", }); - - // TODO: 4.: source has been deleted: be an op - + 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, @@ -1333,7 +1629,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-indexnames get indexNames() { webidl.assertBranded(this, IDBObjectStorePrototype); - // TODO: add indexset to this + return [...this[_indexSet].keys()]; } [_transaction]; @@ -1392,7 +1688,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1409,7 +1705,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbobjectstore-clear clear() { webidl.assertBranded(this, IDBObjectStorePrototype); - // TODO: 3.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1431,7 +1727,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1451,7 +1747,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1477,7 +1773,7 @@ enforceRange: true, }); } - // TODO: 3.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1503,7 +1799,7 @@ enforceRange: true, }); } - // TODO: 3.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1522,7 +1818,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1545,7 +1841,7 @@ prefix, context: "Argument 2", }); - // TODO: 3.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1577,7 +1873,7 @@ prefix, context: "Argument 2", }); - // TODO: 3.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1606,11 +1902,18 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] === "finished") { throw new DOMException("", "InvalidStateError"); } - // TODO: check indexset on this + 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 @@ -1633,11 +1936,12 @@ if (this[_transaction][_mode] !== "versionchange") { throw new DOMException("", "InvalidStateError"); } - // TODO: 4.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 6.: op + // 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"); } @@ -1645,11 +1949,11 @@ throw new DOMException("", "InvalidAccessError"); } // TODO: 11.: ops - // TODO: 12.: indexset on this 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; @@ -1668,7 +1972,7 @@ if (this[_transaction][_mode] !== "versionchange") { throw new DOMException("", "InvalidStateError"); } - // TODO: 4.: source has been deleted: be an op + assertObjectStore(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1679,10 +1983,17 @@ 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", - // TODO: args (index needs to be structured properly) + index[_storeHandle][_transaction][_connection][_name], + index[_storeHandle][_store][_name], + index[_index][_name], + range, ); if (val === null) { return undefined; @@ -1695,7 +2006,11 @@ function retrieveMultipleReferencedValuesFromIndex(index, range, count) { const vals = core.opSync( "op_indexeddb_index_retrieve_multiple_values", - // TODO: args (index needs to be structured properly) + index[_storeHandle][_transaction][_connection][_name], + index[_storeHandle][_store][_name], + index[_index][_name], + range, + count, ); return vals.map((val) => core.deserialize(val)); } @@ -1704,7 +2019,10 @@ function retrieveValueFromIndex(index, range) { const val = core.opSync( "op_indexeddb_index_retrieve_value", - // TODO: args (index needs to be structured properly) + index[_storeHandle][_transaction][_connection][_name], + index[_storeHandle][_store][_name], + index[_index][_name], + range, ); if (val === undefined) { return undefined; @@ -1717,7 +2035,11 @@ function retrieveMultipleValuesFromIndex(index, range, count) { const vals = core.opSync( "op_indexeddb_index_retrieve_multiple_values", - // TODO: args (index needs to be structured properly) + index[_storeHandle][_transaction][_connection][_name], + index[_storeHandle][_store][_name], + index[_index][_name], + range, + count, ); return vals.map((val) => keyToValue(val)); } @@ -1767,9 +2089,10 @@ throw new DOMException("", "TransactionInactiveError"); } - // TODO: 6.: source has been deleted: be an op + assertIndex(this); + // TODO: 7.: should it be this's _name? or this's _index's name - // TODO: 8.: op? or cache? + // TODO: 8.: cache this[_index].name = name; this[_name] = name; @@ -1808,14 +2131,14 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted: be an op + assertIndex(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } const range = valueToKeyRange(query, true); return asynchronouslyExecuteRequest( this, - () => retrieveReferencedValueFromIndex(this[_index], range), + () => retrieveReferencedValueFromIndex(this, range), ); } @@ -1828,14 +2151,14 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted: be an op + assertIndex(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } const range = valueToKeyRange(query, true); return asynchronouslyExecuteRequest( this, - () => retrieveValueFromIndex(this[_index], range), + () => retrieveValueFromIndex(this, range), ); } @@ -1854,15 +2177,14 @@ enforceRange: true, }); } - // TODO: 3.: source has been deleted: be an op + assertIndex(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } const range = valueToKeyRange(query, true); return asynchronouslyExecuteRequest( this, - () => - retrieveMultipleReferencedValuesFromIndex(this[_index], range, count), + () => retrieveMultipleReferencedValuesFromIndex(this, range, count), ); } @@ -1881,14 +2203,14 @@ enforceRange: true, }); } - // TODO: 3.: source has been deleted: be an op + assertIndex(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } const range = valueToKeyRange(query, true); return asynchronouslyExecuteRequest( this, - () => retrieveMultipleValuesFromIndex(this[_index], range, count), + () => retrieveMultipleValuesFromIndex(this, range, count), ); } @@ -1900,7 +2222,7 @@ prefix, context: "Argument 1", }); - // TODO: 3.: source has been deleted: be an op + assertIndex(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1923,7 +2245,7 @@ prefix, context: "Argument 2", }); - // TODO: 3.: source has been deleted: be an op + assertIndex(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -1955,7 +2277,7 @@ prefix, context: "Argument 2", }); - // TODO: 3.: source has been deleted: be an op + assertIndex(this); if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } @@ -2261,7 +2583,7 @@ if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 4.: source has been deleted: be an op + assertCursor(this); if (!this[_gotValue]) { throw new DOMException("", "InvalidStateError"); } @@ -2287,7 +2609,7 @@ if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 4.: source has been deleted: be an op + assertCursor(this); if (key !== undefined) { key = valueToKey(key); if (key === null) { @@ -2333,7 +2655,7 @@ if (this[_transaction][_state] !== "active") { throw new DOMException("", "TransactionInactiveError"); } - // TODO: 3.: source has been deleted: be an op + assertCursor(this); if (!(this[_source] instanceof IDBIndex)) { throw new DOMException("", "InvalidAccessError"); } @@ -2403,7 +2725,7 @@ if (this[_transaction][_mode] === "readonly") { throw new DOMException("", "ReadOnlyError"); } - // TODO: 4.: source has been deleted: be an op + assertCursor(this); if (!this[_gotValue]) { throw new DOMException("", "InvalidStateError"); } @@ -2442,7 +2764,7 @@ if (this[_transaction][_mode] === "readonly") { throw new DOMException("", "ReadOnlyError"); } - // TODO: 4.: source has been deleted: be an op + assertCursor(this); if (!this[_gotValue]) { throw new DOMException("", "InvalidStateError"); } @@ -2467,8 +2789,9 @@ const _state = Symbol("[[state]]"); const _mode = Symbol("[[mode]]"); const _durabilityHint = Symbol("[[durabilityHint]]"); - const _db = Symbol("[[db]]"); const _rid = Symbol("[[rid]]"); + const _connection = Symbol("[[connection]]"); + const _scope = Symbol("[[scope]]"); // Ref: https://w3c.github.io/IndexedDB/#idbtransaction class IDBTransaction extends EventTarget { [_rid]; @@ -2479,7 +2802,8 @@ [_mode]; [_durabilityHint]; [_error]; - [_db]; + [_connection]; + [_scope]; constructor() { super(); @@ -2507,7 +2831,7 @@ // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-db get db() { webidl.assertBranded(this, IDBTransactionPrototype); - return this[_db]; + return this[_connection]; } // Ref: https://w3c.github.io/IndexedDB/#dom-idbtransaction-error diff --git a/ext/webstorage/Cargo.toml b/ext/webstorage/Cargo.toml index 6b5e7d5a73ac8e..24233e887c654a 100644 --- a/ext/webstorage/Cargo.toml +++ b/ext/webstorage/Cargo.toml @@ -18,5 +18,5 @@ async-trait = "0.1.52" deno_core = { version = "0.124.0", path = "../../core" } deno_web = { version = "0.73.0", path = "../web" } fallible-iterator = "0.2.0" -rusqlite = { version = "0.25.3", features = ["unlock_notify", "bundled"] } +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 index 6aa12aa38f0fa8..fb770d0aaf0747 100644 --- a/ext/webstorage/indexeddb/mod.rs +++ b/ext/webstorage/indexeddb/mod.rs @@ -1,6 +1,6 @@ use super::DomExceptionNotSupportedError; use super::OriginStorageDir; -use crate::DomExceptionConstraintError; +use crate::{DomExceptionConstraintError, DomExceptionInvalidStateError}; use deno_core::error::AnyError; use deno_core::op; use deno_core::serde_json; @@ -201,7 +201,7 @@ pub fn op_indexeddb_database_create_object_store( state: &mut OpState, database_name: String, name: String, - key_path: Option, + key_path: serde_json::Value, ) -> Result<(), AnyError> { let conn = &state.borrow::().0; @@ -232,6 +232,29 @@ pub fn op_indexeddb_database_create_object_store( 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( @@ -774,6 +797,36 @@ pub fn op_indexeddb_object_store_count_records( 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( @@ -824,6 +877,7 @@ pub fn op_indexeddb_index_retrieve_multiple_values( store_name: String, index_name: String, range: Range, + count: Option, ) -> Result, AnyError> { let conn = &state.borrow::().0; @@ -843,3 +897,38 @@ pub fn op_indexeddb_index_retrieve_multiple_values( Ok(None) } + +#[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 13b32a6e0166cc..be79da4142e815 100644 --- a/ext/webstorage/lib.rs +++ b/ext/webstorage/lib.rs @@ -122,3 +122,31 @@ pub fn get_constraint_error_class_name(e: &AnyError) -> Option<&'static str> { e.downcast_ref::() .map(|_| "DOMExceptionConstraintError") } + +#[derive(Debug)] +pub struct DomExceptionInvalidStateError { + pub msg: String, +} + +impl DomExceptionInvalidStateError { + pub fn new(msg: &str) -> Self { + DomExceptionInvalidStateError { + msg: msg.to_string(), + } + } +} + +impl fmt::Display for DomExceptionInvalidStateError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.pad(&self.msg) + } +} + +impl std::error::Error for DomExceptionInvalidStateError {} + +pub fn get_invalid_state_error_class_name( + e: &AnyError, +) -> Option<&'static str> { + e.downcast_ref::() + .map(|_| "DOMExceptionInvalidStateError") +} diff --git a/runtime/errors.rs b/runtime/errors.rs index e6cc0c216916ee..682bb06f2aa808 100644 --- a/runtime/errors.rs +++ b/runtime/errors.rs @@ -159,6 +159,7 @@ pub fn get_error_class_name(e: &AnyError) -> Option<&'static str> { .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::() From 73565175978a6dcf80e306dfa01566ab886c05cb Mon Sep 17 00:00:00 2001 From: crowlkats Date: Wed, 30 Mar 2022 19:13:10 +0200 Subject: [PATCH 14/14] fix --- ext/webstorage/02_indexeddb.js | 32 ++++++------- ext/webstorage/indexeddb/mod.rs | 83 +++++++++++++++++++++------------ 2 files changed, 68 insertions(+), 47 deletions(-) diff --git a/ext/webstorage/02_indexeddb.js b/ext/webstorage/02_indexeddb.js index 6abb49ae0f1d8b..64b8a6f964dadc 100644 --- a/ext/webstorage/02_indexeddb.js +++ b/ext/webstorage/02_indexeddb.js @@ -305,7 +305,7 @@ return true; } else { } - // TODO: complete implementation + // TODO: figure out IdentifierName (https://tc39.es/ecma262/#prod-IdentifierName) } else if (ArrayIsArray(key)) { return key.every(isValidKeyPath); } else { @@ -656,7 +656,7 @@ connection[_upgradeTransaction] = transaction; transaction[_state] = "inactive"; // TODO: 6.: start transaction (call op_indexeddb_transaction_create) - const oldVersion = connection[_db].version; + const oldVersion = connection[_version]; // TODO: 8.: change db version request[_processedDeferred].resolve(); @@ -879,18 +879,6 @@ webidl.configurePrototype(IDBFactory); const IDBFactoryPrototype = IDBFactory.prototype; - // Ref: https://w3c.github.io/IndexedDB/#database-connection - class Connection { - /** @type {IDBDatabase} */ - database; - /** @type {number} */ - version; - /** @type {boolean} */ - closePending = false; - - objectStoreSet; // TODO - } - class Database { /** @type {number} */ version; @@ -909,6 +897,7 @@ 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 { @@ -920,6 +909,8 @@ [_db]; /** @type {boolean} */ [_closePending] = false; + /** @type {IDBTransaction[]} */ + [_transactions] = []; /** @type {Map} */ [_objectStoreSet]; // TODO: update on upgrade transaction @@ -994,6 +985,7 @@ 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; } @@ -1010,9 +1002,13 @@ [_close](forced) { this[_closePending] = true; if (forced) { - // TODO: 2: somehow get all transactions + 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 } - // TODO: 3.: somehow get all transactions if (forced) { this.dispatchEvent(new Event("close")); } @@ -1073,7 +1069,7 @@ store.database = this; store.keyPath = keypath; const objectStore = webidl.createBranded(IDBObjectStore); - objectStore[_name] = name; // TODO: objectstore name is inconsistent throughout the spec + objectStore[_name] = name; objectStore[_store] = store; objectStore[_transaction] = this[_upgradeTransaction]; return objectStore; @@ -1102,7 +1098,7 @@ throw new DOMException("", "NotFoundError"); } MapPrototypeDelete(this[_objectStoreSet], name); - + core.opSync("op_indexeddb_database_delete_object_store", this[_name], store[_name]); // TODO 6.: ops } } diff --git a/ext/webstorage/indexeddb/mod.rs b/ext/webstorage/indexeddb/mod.rs index fb770d0aaf0747..2a0dde8a578725 100644 --- a/ext/webstorage/indexeddb/mod.rs +++ b/ext/webstorage/indexeddb/mod.rs @@ -1,8 +1,10 @@ +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; +use deno_core::{op, Resource}; use deno_core::serde_json; use deno_core::OpState; use deno_core::ResourceId; @@ -17,6 +19,7 @@ use rusqlite::OptionalExtension; use serde::Deserialize; use serde::Serialize; use std::cmp::Ordering; +use std::rc::Rc; #[derive(Clone)] struct Database { @@ -24,27 +27,32 @@ struct Database { version: u64, } -pub struct IndexedDbManager(Connection); +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> { - if state.try_borrow::().is_none() { - 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 = " + 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)?; + conn.execute_batch(initial_pragmas)?; - let create_statements = r#" + let create_statements = r#" CREATE TABLE IF NOT EXISTS database ( name TEXT PRIMARY KEY, version INTEGER NOT NULL DEFAULT 0 @@ -105,24 +113,22 @@ pub fn op_indexeddb_open(state: &mut OpState) -> Result<(), AnyError> { REFERENCES record(object_store_id, key) ) WITHOUT ROWID; "#; - conn.execute_batch(create_statements)?; + conn.execute_batch(create_statements)?; - conn.set_prepared_statement_cache_capacity(128); - state.put(IndexedDbManager(conn)); - } + conn.set_prepared_statement_cache_capacity(128); + state.resource_table.add(IndexedDb(Rc::new(RefCell::new(conn)), None)); Ok(()) } -pub struct Transaction(rusqlite::Transaction<'static>); - #[op] pub fn op_indexeddb_transaction_create( state: &mut OpState, ) -> Result { - let idbmanager = state.borrow_mut::(); - let transaction = idbmanager.0.transaction()?; - let rid = state.resource_table.add(Transaction(transaction)); + 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) } @@ -131,8 +137,8 @@ pub fn op_indexeddb_transaction_commit( state: &mut OpState, rid: ResourceId, ) -> Result<(), AnyError> { - let rid = state.resource_table.take::(rid)?; - rid.0.commit()?; + let idb = Rc::try_unwrap(state.resource_table.take::(rid)?).unwrap(); + idb.1.commit()?; Ok(()) } @@ -141,8 +147,8 @@ pub fn op_indexeddb_transaction_abort( state: &mut OpState, rid: ResourceId, ) -> Result<(), AnyError> { - let rid = state.resource_table.take::(rid)?; - rid.0.rollback()?; + let idb = Rc::try_unwrap(state.resource_table.take::(rid)?).unwrap(); + idb.1.rollback()?; Ok(()) } @@ -232,6 +238,25 @@ pub fn op_indexeddb_database_create_object_store( 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, @@ -407,7 +432,7 @@ struct Index { object_store_id: u64, database_name: String, name: String, - key_path: todo!(), + key_path: serde_json::Value, unique: bool, multi_entry: bool, } @@ -786,7 +811,7 @@ pub fn op_indexeddb_object_store_count_records( row .map(|(key, val)| { if range.contains(&key) { - Some(val.into()) + Some(()) } else { None } @@ -895,7 +920,7 @@ pub fn op_indexeddb_index_retrieve_multiple_values( todo!(); - Ok(None) + Ok(Default::default()) } #[derive(Serialize, Clone)]