Skip to content

Commit

Permalink
@uppy/golden-retriever: refactor to modernize the codebase (#4520)
Browse files Browse the repository at this point in the history
Use of async/await rather than `.then` chains, avoid caching promises,
use newer syntax/methods where it makes sense, etc.
  • Loading branch information
aduh95 authored Jun 22, 2023
1 parent 44e5e0e commit a869d2e
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 161 deletions.
216 changes: 122 additions & 94 deletions packages/@uppy/golden-retriever/src/IndexedDBStore.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @type {typeof window.indexedDB}
*/
const indexedDB = typeof window !== 'undefined'
&& (window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB)

Expand All @@ -7,8 +10,13 @@ const DB_NAME = 'uppy-blobs'
const STORE_NAME = 'files' // maybe have a thumbnail store in the future
const DEFAULT_EXPIRY = 24 * 60 * 60 * 1000 // 24 hours
const DB_VERSION = 3
const MiB = 0x10_00_00

// Set default `expires` dates on existing stored blobs.
/**
* Set default `expires` dates on existing stored blobs.
*
* @param {IDBObjectStore} store
*/
function migrateExpiration (store) {
const request = store.openCursor()
request.onsuccess = (event) => {
Expand All @@ -22,11 +30,21 @@ function migrateExpiration (store) {
}
}

/**
* @param {string} dbName
* @returns {Promise<IDBDatabase>}
*/
function connect (dbName) {
const request = indexedDB.open(dbName, DB_VERSION)
return new Promise((resolve, reject) => {
request.onupgradeneeded = (event) => {
/**
* @type {IDBDatabase}
*/
const db = event.target.result
/**
* @type {IDBTransaction}
*/
const { transaction } = event.currentTarget

if (event.oldVersion < 2) {
Expand Down Expand Up @@ -54,6 +72,11 @@ function connect (dbName) {
})
}

/**
* @template T
* @param {IDBRequest<T>} request
* @returns {Promise<T>}
*/
function waitForRequest (request) {
return new Promise((resolve, reject) => {
request.onsuccess = (event) => {
Expand All @@ -65,160 +88,165 @@ function waitForRequest (request) {

let cleanedUp = false
class IndexedDBStore {
/**
* @type {Promise<IDBDatabase> | IDBDatabase}
*/
#ready

constructor (opts) {
this.opts = {
dbName: DB_NAME,
storeName: 'default',
expires: DEFAULT_EXPIRY, // 24 hours
maxFileSize: 10 * 1024 * 1024, // 10 MB
maxTotalSize: 300 * 1024 * 1024, // 300 MB
maxFileSize: 10 * MiB,
maxTotalSize: 300 * MiB,
...opts,
}

this.name = this.opts.storeName

const createConnection = () => {
return connect(this.opts.dbName)
const createConnection = async () => {
const db = await connect(this.opts.dbName)
this.#ready = db
return db
}

if (!cleanedUp) {
cleanedUp = true
this.ready = IndexedDBStore.cleanup()
this.#ready = IndexedDBStore.cleanup()
.then(createConnection, createConnection)
} else {
this.ready = createConnection()
this.#ready = createConnection()
}
}

get ready () {
return Promise.resolve(this.#ready)
}

// TODO: remove this setter in the next major
set ready (val) {
this.#ready = val
}

key (fileID) {
return `${this.name}!${fileID}`
}

/**
* List all file blobs currently in the store.
*/
list () {
return this.ready.then((db) => {
const transaction = db.transaction([STORE_NAME], 'readonly')
const store = transaction.objectStore(STORE_NAME)
const request = store.index('store')
.getAll(IDBKeyRange.only(this.name))
return waitForRequest(request)
}).then((files) => {
const result = {}
files.forEach((file) => {
result[file.fileID] = file.data
})
return result
})
async list () {
const db = await this.#ready
const transaction = db.transaction([STORE_NAME], 'readonly')
const store = transaction.objectStore(STORE_NAME)
const request = store.index('store')
.getAll(IDBKeyRange.only(this.name))
const files = await waitForRequest(request)
return Object.fromEntries(files.map(file => [file.fileID, file.data]))
}

/**
* Get one file blob from the store.
*/
get (fileID) {
return this.ready.then((db) => {
const transaction = db.transaction([STORE_NAME], 'readonly')
const request = transaction.objectStore(STORE_NAME)
.get(this.key(fileID))
return waitForRequest(request)
}).then((result) => ({
id: result.data.fileID,
data: result.data.data,
}))
async get (fileID) {
const db = await this.#ready
const transaction = db.transaction([STORE_NAME], 'readonly')
const request = transaction.objectStore(STORE_NAME)
.get(this.key(fileID))
const { data } = await waitForRequest(request)
return {
id: data.fileID,
data: data.data,
}
}

/**
* Get the total size of all stored files.
*
* @private
* @returns {Promise<number>}
*/
getSize () {
return this.ready.then((db) => {
const transaction = db.transaction([STORE_NAME], 'readonly')
const store = transaction.objectStore(STORE_NAME)
const request = store.index('store')
.openCursor(IDBKeyRange.only(this.name))
return new Promise((resolve, reject) => {
let size = 0
request.onsuccess = (event) => {
const cursor = event.target.result
if (cursor) {
size += cursor.value.data.size
cursor.continue()
} else {
resolve(size)
}
}
request.onerror = () => {
reject(new Error('Could not retrieve stored blobs size'))
async getSize () {
const db = await this.#ready
const transaction = db.transaction([STORE_NAME], 'readonly')
const store = transaction.objectStore(STORE_NAME)
const request = store.index('store')
.openCursor(IDBKeyRange.only(this.name))
return new Promise((resolve, reject) => {
let size = 0
request.onsuccess = (event) => {
const cursor = event.target.result
if (cursor) {
size += cursor.value.data.size
cursor.continue()
} else {
resolve(size)
}
})
}
request.onerror = () => {
reject(new Error('Could not retrieve stored blobs size'))
}
})
}

/**
* Save a file in the store.
*/
put (file) {
async put (file) {
if (file.data.size > this.opts.maxFileSize) {
return Promise.reject(new Error('File is too big to store.'))
throw new Error('File is too big to store.')
}
return this.getSize().then((size) => {
if (size > this.opts.maxTotalSize) {
return Promise.reject(new Error('No space left'))
}
return this.ready
}).then((db) => {
const transaction = db.transaction([STORE_NAME], 'readwrite')
const request = transaction.objectStore(STORE_NAME).add({
id: this.key(file.id),
fileID: file.id,
store: this.name,
expires: Date.now() + this.opts.expires,
data: file.data,
})
return waitForRequest(request)
const size = await this.getSize()
if (size > this.opts.maxTotalSize) {
throw new Error('No space left')
}
const db = this.#ready
const transaction = db.transaction([STORE_NAME], 'readwrite')
const request = transaction.objectStore(STORE_NAME).add({
id: this.key(file.id),
fileID: file.id,
store: this.name,
expires: Date.now() + this.opts.expires,
data: file.data,
})
return waitForRequest(request)
}

/**
* Delete a file blob from the store.
*/
delete (fileID) {
return this.ready.then((db) => {
const transaction = db.transaction([STORE_NAME], 'readwrite')
const request = transaction.objectStore(STORE_NAME)
.delete(this.key(fileID))
return waitForRequest(request)
})
async delete (fileID) {
const db = await this.#ready
const transaction = db.transaction([STORE_NAME], 'readwrite')
const request = transaction.objectStore(STORE_NAME)
.delete(this.key(fileID))
return waitForRequest(request)
}

/**
* Delete all stored blobs that have an expiry date that is before Date.now().
* This is a static method because it deletes expired blobs from _all_ Uppy instances.
*/
static cleanup () {
return connect(DB_NAME).then((db) => {
const transaction = db.transaction([STORE_NAME], 'readwrite')
const store = transaction.objectStore(STORE_NAME)
const request = store.index('expires')
.openCursor(IDBKeyRange.upperBound(Date.now()))
return new Promise((resolve, reject) => {
request.onsuccess = (event) => {
const cursor = event.target.result
if (cursor) {
cursor.delete() // Ignoring return value … it's not terrible if this goes wrong.
cursor.continue()
} else {
resolve(db)
}
static async cleanup () {
const db = await connect(DB_NAME)
const transaction = db.transaction([STORE_NAME], 'readwrite')
const store = transaction.objectStore(STORE_NAME)
const request = store.index('expires')
.openCursor(IDBKeyRange.upperBound(Date.now()))
await new Promise((resolve, reject) => {
request.onsuccess = (event) => {
const cursor = event.target.result
if (cursor) {
cursor.delete() // Ignoring return value … it's not terrible if this goes wrong.
cursor.continue()
} else {
resolve()
}
request.onerror = reject
})
}).then((db) => {
db.close()
}
request.onerror = reject
})
db.close()
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/@uppy/golden-retriever/src/MetaDataStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ function findUppyInstances () {
const instances = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (/^uppyState:/.test(key)) {
if (key.startsWith('uppyState:')) {
instances.push(key.slice('uppyState:'.length))
}
}
Expand All @@ -18,7 +18,7 @@ function findUppyInstances () {
function maybeParse (str) {
try {
return JSON.parse(str)
} catch (err) {
} catch {
return null
}
}
Expand Down
4 changes: 1 addition & 3 deletions packages/@uppy/golden-retriever/src/ServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
const fileCache = Object.create(null)

function getCache (name) {
if (!fileCache[name]) {
fileCache[name] = Object.create(null)
}
fileCache[name] ??= Object.create(null)
return fileCache[name]
}

Expand Down
Loading

0 comments on commit a869d2e

Please sign in to comment.