Bläddra i källkod

@uppy/golden-retriever: migrate to TS (#4989)

Co-authored-by: Antoine du Hamel <antoine@transloadit.com>
Merlijn Vos 1 år sedan
förälder
incheckning
8ae90c5b11

+ 1 - 0
packages/@uppy/golden-retriever/.npmignore

@@ -0,0 +1 @@
+tsconfig.*

+ 76 - 66
packages/@uppy/golden-retriever/src/IndexedDBStore.js → packages/@uppy/golden-retriever/src/IndexedDBStore.ts

@@ -1,8 +1,16 @@
-/**
- * @type {typeof window.indexedDB}
- */
-const indexedDB = typeof window !== 'undefined'
-  && (window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.OIndexedDB || window.msIndexedDB)
+import type { UppyFile } from '@uppy/utils/lib/UppyFile'
+
+const indexedDB =
+  typeof window !== 'undefined' &&
+  (window.indexedDB ||
+    // @ts-expect-error unknown
+    window.webkitIndexedDB ||
+    // @ts-expect-error unknown
+    window.mozIndexedDB ||
+    // @ts-expect-error unknown
+    window.OIndexedDB ||
+    // @ts-expect-error unknown
+    window.msIndexedDB)
 
 const isSupported = !!indexedDB
 
@@ -14,13 +22,11 @@ const MiB = 0x10_00_00
 
 /**
  * Set default `expires` dates on existing stored blobs.
- *
- * @param {IDBObjectStore} store
  */
-function migrateExpiration (store) {
+function migrateExpiration(store: IDBObjectStore) {
   const request = store.openCursor()
   request.onsuccess = (event) => {
-    const cursor = event.target.result
+    const cursor = (event.target as IDBRequest).result
     if (!cursor) {
       return
     }
@@ -30,22 +36,14 @@ function migrateExpiration (store) {
   }
 }
 
-/**
- * @param {string} dbName
- * @returns {Promise<IDBDatabase>}
- */
-function connect (dbName) {
-  const request = indexedDB.open(dbName, DB_VERSION)
+function connect(dbName: string): Promise<IDBDatabase> {
+  const request = (indexedDB as IDBFactory).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
+      const db: IDBDatabase = (event.target as IDBOpenDBRequest).result
+      // eslint-disable-next-line prefer-destructuring
+      const transaction = (event.currentTarget as IDBOpenDBRequest)
+        .transaction as IDBTransaction
 
       if (event.oldVersion < 2) {
         // Added in v2: DB structure changed to a single shared object store
@@ -66,34 +64,48 @@ function connect (dbName) {
       }
     }
     request.onsuccess = (event) => {
-      resolve(event.target.result)
+      resolve((event.target as IDBRequest).result)
     }
     request.onerror = reject
   })
 }
 
-/**
- * @template T
- * @param {IDBRequest<T>} request
- * @returns {Promise<T>}
- */
-function waitForRequest (request) {
+function waitForRequest<T>(request: IDBRequest): Promise<T> {
   return new Promise((resolve, reject) => {
     request.onsuccess = (event) => {
-      resolve(event.target.result)
+      resolve((event.target as IDBRequest).result)
     }
     request.onerror = reject
   })
 }
 
+type IndexedDBStoredFile = {
+  id: string
+  fileID: string
+  store: string
+  expires: number
+  data: Blob
+}
+
+type IndexedDBStoreOptions = {
+  dbName?: string
+  storeName?: string
+  expires?: number
+  maxFileSize?: number
+  maxTotalSize?: number
+}
+
 let cleanedUp = false
 class IndexedDBStore {
-  /**
-   * @type {Promise<IDBDatabase> | IDBDatabase}
-   */
-  #ready
+  #ready: Promise<IDBDatabase> | IDBDatabase
 
-  constructor (opts) {
+  opts: Required<IndexedDBStoreOptions>
+
+  name: string
+
+  static isSupported: boolean
+
+  constructor(opts?: IndexedDBStoreOptions) {
     this.opts = {
       dbName: DB_NAME,
       storeName: 'default',
@@ -113,48 +125,50 @@ class IndexedDBStore {
 
     if (!cleanedUp) {
       cleanedUp = true
-      this.#ready = IndexedDBStore.cleanup()
-        .then(createConnection, createConnection)
+      this.#ready = IndexedDBStore.cleanup().then(
+        createConnection,
+        createConnection,
+      )
     } else {
       this.#ready = createConnection()
     }
   }
 
-  get ready () {
+  get ready(): Promise<IDBDatabase> {
     return Promise.resolve(this.#ready)
   }
 
   // TODO: remove this setter in the next major
-  set ready (val) {
+  set ready(val: IDBDatabase) {
     this.#ready = val
   }
 
-  key (fileID) {
+  key(fileID: string): string {
     return `${this.name}!${fileID}`
   }
 
   /**
    * List all file blobs currently in the store.
    */
-  async list () {
+  async list(): Promise<Record<string, IndexedDBStoredFile['data']>> {
     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]))
+    const request = store.index('store').getAll(IDBKeyRange.only(this.name))
+    const files = await waitForRequest<IndexedDBStoredFile[]>(request)
+    return Object.fromEntries(files.map((file) => [file.fileID, file.data]))
   }
 
   /**
    * Get one file blob from the store.
    */
-  async get (fileID) {
+  async get(fileID: string): Promise<{ id: string; data: Blob }> {
     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)
+    const request = transaction.objectStore(STORE_NAME).get(this.key(fileID))
+    const { data } = await waitForRequest<{
+      data: { data: Blob; fileID: string }
+    }>(request)
     return {
       id: data.fileID,
       data: data.data,
@@ -163,20 +177,16 @@ class IndexedDBStore {
 
   /**
    * Get the total size of all stored files.
-   *
-   * @private
-   * @returns {Promise<number>}
    */
-  async getSize () {
+  async getSize(): Promise<number> {
     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))
+    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
+        const cursor = (event.target as IDBRequest).result
         if (cursor) {
           size += cursor.value.data.size
           cursor.continue()
@@ -193,7 +203,7 @@ class IndexedDBStore {
   /**
    * Save a file in the store.
    */
-  async put (file) {
+  async put<T>(file: UppyFile<any, any>): Promise<T> {
     if (file.data.size > this.opts.maxFileSize) {
       throw new Error('File is too big to store.')
     }
@@ -201,7 +211,7 @@ class IndexedDBStore {
     if (size > this.opts.maxTotalSize) {
       throw new Error('No space left')
     }
-    const db = this.#ready
+    const db = await this.#ready
     const transaction = db.transaction([STORE_NAME], 'readwrite')
     const request = transaction.objectStore(STORE_NAME).add({
       id: this.key(file.id),
@@ -216,11 +226,10 @@ class IndexedDBStore {
   /**
    * Delete a file blob from the store.
    */
-  async delete (fileID) {
+  async delete(fileID: string): Promise<unknown> {
     const db = await this.#ready
     const transaction = db.transaction([STORE_NAME], 'readwrite')
-    const request = transaction.objectStore(STORE_NAME)
-      .delete(this.key(fileID))
+    const request = transaction.objectStore(STORE_NAME).delete(this.key(fileID))
     return waitForRequest(request)
   }
 
@@ -228,15 +237,16 @@ class IndexedDBStore {
    * 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 async cleanup () {
+  static async cleanup(): Promise<void> {
     const db = await connect(DB_NAME)
     const transaction = db.transaction([STORE_NAME], 'readwrite')
     const store = transaction.objectStore(STORE_NAME)
-    const request = store.index('expires')
+    const request = store
+      .index('expires')
       .openCursor(IDBKeyRange.upperBound(Date.now()))
-    await new Promise((resolve, reject) => {
+    await new Promise<void>((resolve, reject) => {
       request.onsuccess = (event) => {
-        const cursor = event.target.result
+        const cursor = (event.target as IDBRequest).result
         if (cursor) {
           cursor.delete() // Ignoring return value … it's not terrible if this goes wrong.
           cursor.continue()

+ 33 - 17
packages/@uppy/golden-retriever/src/MetaDataStore.js → packages/@uppy/golden-retriever/src/MetaDataStore.ts

@@ -1,11 +1,23 @@
+import type { State as UppyState } from '@uppy/core'
+import type { Meta, Body } from '@uppy/utils/lib/UppyFile'
+
+export type StoredState<M extends Meta, B extends Body> = {
+  expires: number
+  metadata: {
+    currentUploads: UppyState<M, B>['currentUploads']
+    files: UppyState<M, B>['files']
+    pluginData: Record<string, unknown>
+  }
+}
+
 /**
  * Get uppy instance IDs for which state is stored.
  */
-function findUppyInstances () {
-  const instances = []
+function findUppyInstances(): string[] {
+  const instances: string[] = []
   for (let i = 0; i < localStorage.length; i++) {
     const key = localStorage.key(i)
-    if (key.startsWith('uppyState:')) {
+    if (key?.startsWith('uppyState:')) {
       instances.push(key.slice('uppyState:'.length))
     }
   }
@@ -15,7 +27,9 @@ function findUppyInstances () {
 /**
  * Try to JSON-parse a string, return null on failure.
  */
-function maybeParse (str) {
+function maybeParse<M extends Meta, B extends Body>(
+  str: string,
+): StoredState<M, B> | null {
   try {
     return JSON.parse(str)
   } catch {
@@ -23,9 +37,18 @@ function maybeParse (str) {
   }
 }
 
+type MetaDataStoreOptions = {
+  storeName: string
+  expires?: number
+}
+
 let cleanedUp = false
-export default class MetaDataStore {
-  constructor (opts) {
+export default class MetaDataStore<M extends Meta, B extends Body> {
+  opts: Required<MetaDataStoreOptions>
+
+  name: string
+
+  constructor(opts: MetaDataStoreOptions) {
     this.opts = {
       expires: 24 * 60 * 60 * 1000, // 24 hours
       ...opts,
@@ -41,23 +64,16 @@ export default class MetaDataStore {
   /**
    *
    */
-  load () {
+  load(): StoredState<M, B>['metadata'] | null {
     const savedState = localStorage.getItem(this.name)
     if (!savedState) return null
-    const data = maybeParse(savedState)
+    const data = maybeParse<M, B>(savedState)
     if (!data) return null
 
-    // Upgrade pre-0.20.0 uppyState: it used to be just a flat object,
-    // without `expires`.
-    if (!data.metadata) {
-      this.save(data)
-      return data
-    }
-
     return data.metadata
   }
 
-  save (metadata) {
+  save(metadata: Record<string, unknown>): void {
     const expires = Date.now() + this.opts.expires
     const state = JSON.stringify({
       metadata,
@@ -69,7 +85,7 @@ export default class MetaDataStore {
   /**
    * Remove all expired state.
    */
-  static cleanup (instanceID) {
+  static cleanup(instanceID?: string): void {
     if (instanceID) {
       localStorage.removeItem(`uppyState:${instanceID}`)
       return

+ 35 - 16
packages/@uppy/golden-retriever/src/ServiceWorkerStore.js → packages/@uppy/golden-retriever/src/ServiceWorkerStore.ts

@@ -1,7 +1,10 @@
-const isSupported = typeof navigator !== 'undefined' && 'serviceWorker' in navigator
+import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
 
-function waitForServiceWorker () {
-  return new Promise((resolve, reject) => {
+const isSupported =
+  typeof navigator !== 'undefined' && 'serviceWorker' in navigator
+
+function waitForServiceWorker() {
+  return new Promise<void>((resolve, reject) => {
     if (!isSupported) {
       reject(new Error('Unsupported'))
     } else if (navigator.serviceWorker.controller) {
@@ -15,28 +18,44 @@ function waitForServiceWorker () {
   })
 }
 
-class ServiceWorkerStore {
-  #ready
+export type ServiceWorkerStoredFile<M extends Meta, B extends Body> = {
+  type: string
+  store: string
+  file: UppyFile<M, B>
+}
+
+type ServiceWorkerStoreOptions = {
+  storeName: string
+}
+
+class ServiceWorkerStore<M extends Meta, B extends Body> {
+  #ready: void | Promise<void>
+
+  name: string
 
-  constructor (opts) {
-    this.#ready = waitForServiceWorker().then((val) => { this.#ready = val })
+  static isSupported: boolean
+
+  constructor(opts: ServiceWorkerStoreOptions) {
+    this.#ready = waitForServiceWorker().then((val) => {
+      this.#ready = val
+    })
     this.name = opts.storeName
   }
 
-  get ready () {
+  get ready(): Promise<void> {
     return Promise.resolve(this.#ready)
   }
 
   // TODO: remove this setter in the next major
-  set ready (val) {
+  set ready(val: void) {
     this.#ready = val
   }
 
-  async list () {
+  async list(): Promise<ServiceWorkerStoredFile<M, B>[]> {
     await this.#ready
 
     return new Promise((resolve, reject) => {
-      const onMessage = (event) => {
+      const onMessage = (event: MessageEvent) => {
         if (event.data.store !== this.name) {
           return
         }
@@ -52,25 +71,25 @@ class ServiceWorkerStore {
 
       navigator.serviceWorker.addEventListener('message', onMessage)
 
-      navigator.serviceWorker.controller.postMessage({
+      navigator.serviceWorker.controller!.postMessage({
         type: 'uppy/GET_FILES',
         store: this.name,
       })
     })
   }
 
-  async put (file) {
+  async put(file: UppyFile<any, any>): Promise<void> {
     await this.#ready
-    navigator.serviceWorker.controller.postMessage({
+    navigator.serviceWorker.controller!.postMessage({
       type: 'uppy/ADD_FILE',
       store: this.name,
       file,
     })
   }
 
-  async delete (fileID) {
+  async delete(fileID: string): Promise<void> {
     await this.#ready
-    navigator.serviceWorker.controller.postMessage({
+    navigator.serviceWorker.controller!.postMessage({
       type: 'uppy/REMOVE_FILE',
       store: this.name,
       fileID,

+ 0 - 10
packages/@uppy/golden-retriever/src/cleanup.js

@@ -1,10 +0,0 @@
-import IndexedDBStore from './IndexedDBStore.js'
-import MetaDataStore from './MetaDataStore.js'
-
-/**
- * Clean old blobs without needing to import all of Uppy.
- */
-export default function cleanup () {
-  MetaDataStore.cleanup()
-  IndexedDBStore.cleanup()
-}

+ 10 - 0
packages/@uppy/golden-retriever/src/cleanup.ts

@@ -0,0 +1,10 @@
+import IndexedDBStore from './IndexedDBStore.ts'
+import MetaDataStore from './MetaDataStore.ts'
+
+/**
+ * Clean old blobs without needing to import all of Uppy.
+ */
+export default function cleanup(): void {
+  MetaDataStore.cleanup()
+  IndexedDBStore.cleanup()
+}

+ 195 - 100
packages/@uppy/golden-retriever/src/index.js → packages/@uppy/golden-retriever/src/index.ts

@@ -1,11 +1,46 @@
 import throttle from 'lodash/throttle.js'
 import BasePlugin from '@uppy/core/lib/BasePlugin.js'
-import ServiceWorkerStore from './ServiceWorkerStore.js'
-import IndexedDBStore from './IndexedDBStore.js'
-import MetaDataStore from './MetaDataStore.js'
-
+import type { PluginOpts, DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts'
+import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
+import type Uppy from '@uppy/core'
+import type { UploadResult } from '@uppy/core'
+import ServiceWorkerStore, {
+  type ServiceWorkerStoredFile,
+} from './ServiceWorkerStore.ts'
+import IndexedDBStore from './IndexedDBStore.ts'
+import MetaDataStore from './MetaDataStore.ts'
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore We don't want TS to generate types for the package.json
 import packageJson from '../package.json'
 
+declare module '@uppy/core' {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  export interface UppyEventMap<M extends Meta, B extends Body> {
+    // TODO: remove this event
+    'restore:get-data': (fn: (data: Record<string, unknown>) => void) => void
+  }
+}
+
+export interface GoldenRetrieverOptions extends PluginOpts {
+  expires?: number
+  serviceWorker?: boolean
+  indexedDB?: {
+    name?: string
+    version?: number
+  }
+}
+
+const defaultOptions = {
+  expires: 24 * 60 * 60 * 1000, // 24 hours
+  serviceWorker: false,
+}
+
+type Opts = DefinePluginOpts<
+  GoldenRetrieverOptions,
+  keyof typeof defaultOptions
+>
+
 /**
  * The GoldenRetriever plugin — restores selected files and resumes uploads
  * after a closed tab or a browser crash!
@@ -13,21 +48,24 @@ import packageJson from '../package.json'
  * Uses localStorage, IndexedDB and ServiceWorker to do its magic, read more:
  * https://uppy.io/blog/2017/07/golden-retriever/
  */
-export default class GoldenRetriever extends BasePlugin {
+export default class GoldenRetriever<
+  M extends Meta,
+  B extends Body,
+> extends BasePlugin<Opts, M, B> {
   static VERSION = packageJson.version
 
-  constructor (uppy, opts) {
-    super(uppy, opts)
-    this.type = 'debugger'
-    this.id = this.opts.id || 'GoldenRetriever'
-    this.title = 'Golden Retriever'
+  MetaDataStore: MetaDataStore<M, B>
 
-    const defaultOptions = {
-      expires: 24 * 60 * 60 * 1000, // 24 hours
-      serviceWorker: false,
-    }
+  ServiceWorkerStore: ServiceWorkerStore<M, B> | null
 
-    this.opts = { ...defaultOptions, ...opts }
+  IndexedDBStore: IndexedDBStore
+
+  savedPluginData: Record<string, unknown>
+
+  constructor(uppy: Uppy<M, B>, opts?: GoldenRetrieverOptions) {
+    super(uppy, { ...defaultOptions, ...opts })
+    this.type = 'debugger'
+    this.id = this.opts.id || 'GoldenRetriever'
 
     this.MetaDataStore = new MetaDataStore({
       expires: this.opts.expires,
@@ -35,11 +73,13 @@ export default class GoldenRetriever extends BasePlugin {
     })
     this.ServiceWorkerStore = null
     if (this.opts.serviceWorker) {
-      this.ServiceWorkerStore = new ServiceWorkerStore({ storeName: uppy.getID() })
+      this.ServiceWorkerStore = new ServiceWorkerStore({
+        storeName: uppy.getID(),
+      })
     }
     this.IndexedDBStore = new IndexedDBStore({
       expires: this.opts.expires,
-      ...this.opts.indexedDB || {},
+      ...(this.opts.indexedDB || {}),
       storeName: uppy.getID(),
     })
 
@@ -49,12 +89,13 @@ export default class GoldenRetriever extends BasePlugin {
       { leading: true, trailing: true },
     )
     this.restoreState = this.restoreState.bind(this)
-    this.loadFileBlobsFromServiceWorker = this.loadFileBlobsFromServiceWorker.bind(this)
+    this.loadFileBlobsFromServiceWorker =
+      this.loadFileBlobsFromServiceWorker.bind(this)
     this.loadFileBlobsFromIndexedDB = this.loadFileBlobsFromIndexedDB.bind(this)
     this.onBlobsLoaded = this.onBlobsLoaded.bind(this)
   }
 
-  restoreState () {
+  restoreState(): void {
     const savedState = this.MetaDataStore.load()
     if (savedState) {
       this.uppy.log('[GoldenRetriever] Recovered some state from Local Storage')
@@ -71,8 +112,8 @@ export default class GoldenRetriever extends BasePlugin {
    * Get file objects that are currently waiting: they've been selected,
    * but aren't yet being uploaded.
    */
-  getWaitingFiles () {
-    const waitingFiles = {}
+  getWaitingFiles(): Record<string, UppyFile<M, B>> {
+    const waitingFiles: Record<string, UppyFile<M, B>> = {}
 
     this.uppy.getFiles().forEach((file) => {
       if (!file.progress || !file.progress.uploadStarted) {
@@ -88,8 +129,8 @@ export default class GoldenRetriever extends BasePlugin {
    * uploading, but the other files in the same batch have not, the finished
    * file is also returned.
    */
-  getUploadingFiles () {
-    const uploadingFiles = {}
+  getUploadingFiles(): Record<string, UppyFile<M, B>> {
+    const uploadingFiles: Record<string, UppyFile<M, B>> = {}
 
     const { currentUploads } = this.uppy.getState()
     if (currentUploads) {
@@ -105,7 +146,7 @@ export default class GoldenRetriever extends BasePlugin {
     return uploadingFiles
   }
 
-  saveFilesStateToLocalStorage () {
+  saveFilesStateToLocalStorage(): void {
     const filesToSave = {
       ...this.getWaitingFiles(),
       ...this.getUploadingFiles(),
@@ -124,21 +165,25 @@ export default class GoldenRetriever extends BasePlugin {
     // We dont’t need to store file.data on local files, because the actual blob will be restored later,
     // and we want to avoid having weird properties in the serialized object.
     // Also adding file.isRestored to all files, since they will be restored from local storage
-    const filesToSaveWithoutData = Object.fromEntries(fileToSaveEntries.map(([id, fileInfo]) => [id, fileInfo.isRemote
-      ? {
-        ...fileInfo,
-        isRestored: true,
-      }
-      : {
-        ...fileInfo,
-        isRestored: true,
-        data: null,
-        preview: null,
-      },
-    ]))
+    const filesToSaveWithoutData = Object.fromEntries(
+      fileToSaveEntries.map(([id, fileInfo]) => [
+        id,
+        fileInfo.isRemote ?
+          {
+            ...fileInfo,
+            isRestored: true,
+          }
+        : {
+            ...fileInfo,
+            isRestored: true,
+            data: null,
+            preview: null,
+          },
+      ]),
+    )
 
     const pluginData = {}
-    // TODO Find a better way to do this?
+    // TODO Remove this,
     // Other plugins can attach a restore:get-data listener that receives this callback.
     // Plugins can then use this callback (sync) to provide data to be stored.
     this.uppy.emit('restore:get-data', (data) => {
@@ -154,46 +199,64 @@ export default class GoldenRetriever extends BasePlugin {
     })
   }
 
-  loadFileBlobsFromServiceWorker () {
+  loadFileBlobsFromServiceWorker(): Promise<
+    ServiceWorkerStoredFile<M, B> | Record<string, unknown>
+  > {
     if (!this.ServiceWorkerStore) {
       return Promise.resolve({})
     }
 
-    return this.ServiceWorkerStore.list().then((blobs) => {
-      const numberOfFilesRecovered = Object.keys(blobs).length
+    return this.ServiceWorkerStore.list()
+      .then((blobs) => {
+        const numberOfFilesRecovered = Object.keys(blobs).length
 
-      if (numberOfFilesRecovered > 0) {
-        this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`)
-        return blobs
-      }
-      this.uppy.log('[GoldenRetriever] No blobs found in Service Worker, trying IndexedDB now...')
-      return {}
-    }).catch((err) => {
-      this.uppy.log('[GoldenRetriever] Failed to recover blobs from Service Worker', 'warning')
-      this.uppy.log(err)
-      return {}
-    })
+        if (numberOfFilesRecovered > 0) {
+          this.uppy.log(
+            `[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from Service Worker!`,
+          )
+          return blobs
+        }
+        this.uppy.log(
+          '[GoldenRetriever] No blobs found in Service Worker, trying IndexedDB now...',
+        )
+        return {}
+      })
+      .catch((err) => {
+        this.uppy.log(
+          '[GoldenRetriever] Failed to recover blobs from Service Worker',
+          'warning',
+        )
+        this.uppy.log(err)
+        return {}
+      })
   }
 
-  loadFileBlobsFromIndexedDB () {
-    return this.IndexedDBStore.list().then((blobs) => {
-      const numberOfFilesRecovered = Object.keys(blobs).length
+  loadFileBlobsFromIndexedDB(): ReturnType<IndexedDBStore['list']> {
+    return this.IndexedDBStore.list()
+      .then((blobs) => {
+        const numberOfFilesRecovered = Object.keys(blobs).length
 
-      if (numberOfFilesRecovered > 0) {
-        this.uppy.log(`[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from IndexedDB!`)
-        return blobs
-      }
-      this.uppy.log('[GoldenRetriever] No blobs found in IndexedDB')
-      return {}
-    }).catch((err) => {
-      this.uppy.log('[GoldenRetriever] Failed to recover blobs from IndexedDB', 'warning')
-      this.uppy.log(err)
-      return {}
-    })
+        if (numberOfFilesRecovered > 0) {
+          this.uppy.log(
+            `[GoldenRetriever] Successfully recovered ${numberOfFilesRecovered} blobs from IndexedDB!`,
+          )
+          return blobs
+        }
+        this.uppy.log('[GoldenRetriever] No blobs found in IndexedDB')
+        return {}
+      })
+      .catch((err) => {
+        this.uppy.log(
+          '[GoldenRetriever] Failed to recover blobs from IndexedDB',
+          'warning',
+        )
+        this.uppy.log(err)
+        return {}
+      })
   }
 
-  onBlobsLoaded (blobs) {
-    const obsoleteBlobs = []
+  onBlobsLoaded(blobs: Record<string, Blob>): void {
+    const obsoleteBlobs: string[] = []
     const updatedFiles = { ...this.uppy.getState().files }
 
     // Loop through blobs that we can restore, add blobs to file objects
@@ -232,20 +295,33 @@ export default class GoldenRetriever extends BasePlugin {
     this.uppy.emit('restored', this.savedPluginData)
 
     if (obsoleteBlobs.length) {
-      this.deleteBlobs(obsoleteBlobs).then(() => {
-        this.uppy.log(`[GoldenRetriever] Cleaned up ${obsoleteBlobs.length} old files`)
-      }).catch((err) => {
-        this.uppy.log(`[GoldenRetriever] Could not clean up ${obsoleteBlobs.length} old files`, 'warning')
-        this.uppy.log(err)
-      })
+      this.deleteBlobs(obsoleteBlobs)
+        .then(() => {
+          this.uppy.log(
+            `[GoldenRetriever] Cleaned up ${obsoleteBlobs.length} old files`,
+          )
+        })
+        .catch((err) => {
+          this.uppy.log(
+            `[GoldenRetriever] Could not clean up ${obsoleteBlobs.length} old files`,
+            'warning',
+          )
+          this.uppy.log(err)
+        })
     }
   }
 
-  deleteBlobs (fileIDs) {
-    return Promise.all(fileIDs.map(id => this.ServiceWorkerStore?.delete(id) ?? this.IndexedDBStore?.delete(id)))
+  async deleteBlobs(fileIDs: string[]): Promise<void> {
+    await Promise.all(
+      fileIDs.map(
+        (id) =>
+          this.ServiceWorkerStore?.delete(id) ??
+          this.IndexedDBStore?.delete(id),
+      ),
+    )
   }
 
-  addBlobToStores = (file) => {
+  addBlobToStores = (file: UppyFile<M, B>): void => {
     if (file.isRemote) return
 
     if (this.ServiceWorkerStore) {
@@ -261,7 +337,7 @@ export default class GoldenRetriever extends BasePlugin {
     })
   }
 
-  removeBlobFromStores = (file) => {
+  removeBlobFromStores = (file: UppyFile<M, B>): void => {
     if (this.ServiceWorkerStore) {
       this.ServiceWorkerStore.delete(file.id).catch((err) => {
         this.uppy.log('[GoldenRetriever] Failed to remove file', 'warning')
@@ -274,72 +350,90 @@ export default class GoldenRetriever extends BasePlugin {
     })
   }
 
-  replaceBlobInStores = (file) => {
+  replaceBlobInStores = (file: UppyFile<M, B>): void => {
     this.removeBlobFromStores(file)
     this.addBlobToStores(file)
   }
 
-  handleRestoreConfirmed = () => {
+  handleRestoreConfirmed = (): void => {
     this.uppy.log('[GoldenRetriever] Restore confirmed, proceeding...')
     // start all uploads again when file blobs are restored
     const { currentUploads } = this.uppy.getState()
     if (currentUploads) {
       this.uppy.resumeAll()
       Object.keys(currentUploads).forEach((uploadId) => {
-        this.uppy.restore(uploadId, currentUploads[uploadId])
+        this.uppy.restore(uploadId)
       })
     }
     this.uppy.setState({ recoveredState: null })
   }
 
-  abortRestore = () => {
+  abortRestore = (): void => {
     this.uppy.log('[GoldenRetriever] Aborting restore...')
 
     const fileIDs = Object.keys(this.uppy.getState().files)
-    this.deleteBlobs(fileIDs).then(() => {
-      this.uppy.log(`[GoldenRetriever] Removed ${fileIDs.length} files`)
-    }).catch((err) => {
-      this.uppy.log(`[GoldenRetriever] Could not remove ${fileIDs.length} files`, 'warning')
-      this.uppy.log(err)
-    })
+    this.deleteBlobs(fileIDs)
+      .then(() => {
+        this.uppy.log(`[GoldenRetriever] Removed ${fileIDs.length} files`)
+      })
+      .catch((err) => {
+        this.uppy.log(
+          `[GoldenRetriever] Could not remove ${fileIDs.length} files`,
+          'warning',
+        )
+        this.uppy.log(err)
+      })
 
     this.uppy.cancelAll()
     this.uppy.setState({ recoveredState: null })
     MetaDataStore.cleanup(this.uppy.opts.id)
   }
 
-  handleComplete = ({ successful }) => {
-    const fileIDs = successful.map((file) => file.id)
-    this.deleteBlobs(fileIDs).then(() => {
-      this.uppy.log(`[GoldenRetriever] Removed ${successful.length} files that finished uploading`)
-    }).catch((err) => {
-      this.uppy.log(`[GoldenRetriever] Could not remove ${successful.length} files that finished uploading`, 'warning')
-      this.uppy.log(err)
-    })
+  handleComplete = ({ successful }: UploadResult<M, B>): void => {
+    const fileIDs = successful!.map((file) => file.id)
+    this.deleteBlobs(fileIDs)
+      .then(() => {
+        this.uppy.log(
+          `[GoldenRetriever] Removed ${successful!.length} files that finished uploading`,
+        )
+      })
+      .catch((err) => {
+        this.uppy.log(
+          `[GoldenRetriever] Could not remove ${successful!.length} files that finished uploading`,
+          'warning',
+        )
+        this.uppy.log(err)
+      })
 
     this.uppy.setState({ recoveredState: null })
     MetaDataStore.cleanup(this.uppy.opts.id)
   }
 
-  restoreBlobs = () => {
+  restoreBlobs = (): void => {
     if (this.uppy.getFiles().length > 0) {
       Promise.all([
         this.loadFileBlobsFromServiceWorker(),
         this.loadFileBlobsFromIndexedDB(),
       ]).then((resultingArrayOfObjects) => {
-        const blobs = { ...resultingArrayOfObjects[0], ...resultingArrayOfObjects[1] }
+        const blobs = {
+          ...resultingArrayOfObjects[0],
+          ...resultingArrayOfObjects[1],
+        } as Record<string, Blob>
         this.onBlobsLoaded(blobs)
       })
     } else {
-      this.uppy.log('[GoldenRetriever] No files need to be loaded, only restoring processing state...')
+      this.uppy.log(
+        '[GoldenRetriever] No files need to be loaded, only restoring processing state...',
+      )
     }
   }
 
-  install () {
+  install(): void {
     this.restoreState()
     this.restoreBlobs()
 
     this.uppy.on('file-added', this.addBlobToStores)
+    // @ts-expect-error this is typed in @uppy/image-editor and we can't access those types.
     this.uppy.on('file-editor:complete', this.replaceBlobInStores)
     this.uppy.on('file-removed', this.removeBlobFromStores)
     // TODO: the `state-update` is bad practise. It fires on any state change in Uppy
@@ -351,8 +445,9 @@ export default class GoldenRetriever extends BasePlugin {
     this.uppy.on('complete', this.handleComplete)
   }
 
-  uninstall () {
+  uninstall(): void {
     this.uppy.off('file-added', this.addBlobToStores)
+    // @ts-expect-error this is typed in @uppy/image-editor and we can't access those types.
     this.uppy.off('file-editor:complete', this.replaceBlobInStores)
     this.uppy.off('file-removed', this.removeBlobFromStores)
     this.uppy.off('state-update', this.saveFilesStateToLocalStorage)

+ 25 - 0
packages/@uppy/golden-retriever/tsconfig.build.json

@@ -0,0 +1,25 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "noImplicitAny": false,
+    "outDir": "./lib",
+    "paths": {
+      "@uppy/utils/lib/*": ["../utils/src/*"],
+      "@uppy/core": ["../core/src/index.js"],
+      "@uppy/core/lib/*": ["../core/src/*"]
+    },
+    "resolveJsonModule": false,
+    "rootDir": "./src",
+    "skipLibCheck": true
+  },
+  "include": ["./src/**/*.*"],
+  "exclude": ["./src/**/*.test.ts"],
+  "references": [
+    {
+      "path": "../utils/tsconfig.build.json"
+    },
+    {
+      "path": "../core/tsconfig.build.json"
+    }
+  ]
+}

+ 21 - 0
packages/@uppy/golden-retriever/tsconfig.json

@@ -0,0 +1,21 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "emitDeclarationOnly": false,
+    "noEmit": true,
+    "paths": {
+      "@uppy/utils/lib/*": ["../utils/src/*"],
+      "@uppy/core": ["../core/src/index.js"],
+      "@uppy/core/lib/*": ["../core/src/*"],
+    },
+  },
+  "include": ["./package.json", "./src/**/*.*"],
+  "references": [
+    {
+      "path": "../utils/tsconfig.build.json",
+    },
+    {
+      "path": "../core/tsconfig.build.json",
+    },
+  ],
+}