From 82dd7b29a7a71bcd81e3aa401eb05b85b7af8896 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Wed, 13 Mar 2024 15:30:44 +0100 Subject: [PATCH 1/4] @uppy/golden-retriever: migrate to TS --- packages/@uppy/core/src/Uppy.ts | 9 +- packages/@uppy/core/src/index.ts | 1 + packages/@uppy/golden-retriever/.npmignore | 2 + .../{IndexedDBStore.js => IndexedDBStore.ts} | 142 +++++---- .../{MetaDataStore.js => MetaDataStore.ts} | 50 ++- .../{ServiceWorker.js => ServiceWorker.ts} | 29 +- ...ceWorkerStore.js => ServiceWorkerStore.ts} | 51 ++- .../@uppy/golden-retriever/src/cleanup.js | 10 - .../@uppy/golden-retriever/src/cleanup.ts | 10 + .../src/{index.js => index.ts} | 295 ++++++++++++------ .../golden-retriever/tsconfig.build.json | 25 ++ packages/@uppy/golden-retriever/tsconfig.json | 21 ++ packages/@uppy/status-bar/src/Components.tsx | 2 +- packages/@uppy/status-bar/src/StatusBarUI.tsx | 2 +- 14 files changed, 425 insertions(+), 224 deletions(-) create mode 100644 packages/@uppy/golden-retriever/.npmignore rename packages/@uppy/golden-retriever/src/{IndexedDBStore.js => IndexedDBStore.ts} (62%) rename packages/@uppy/golden-retriever/src/{MetaDataStore.js => MetaDataStore.ts} (59%) rename packages/@uppy/golden-retriever/src/{ServiceWorker.js => ServiceWorker.ts} (54%) rename packages/@uppy/golden-retriever/src/{ServiceWorkerStore.js => ServiceWorkerStore.ts} (53%) delete mode 100644 packages/@uppy/golden-retriever/src/cleanup.js create mode 100644 packages/@uppy/golden-retriever/src/cleanup.ts rename packages/@uppy/golden-retriever/src/{index.js => index.ts} (55%) create mode 100644 packages/@uppy/golden-retriever/tsconfig.build.json create mode 100644 packages/@uppy/golden-retriever/tsconfig.json diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index cea4386535..c20c16feee 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -132,7 +132,7 @@ export type UnknownSearchProviderPlugin< provider: CompanionClientSearchProvider } -interface UploadResult { +export interface UploadResult { successful?: UppyFile[] failed?: UppyFile[] uploadID?: string @@ -159,7 +159,7 @@ export interface State } currentUploads: Record> allowNewUpload: boolean - recoveredState: null | State + recoveredState: null | Required, 'files' | 'currentUploads'>> error: string | null files: { [key: string]: UppyFile @@ -317,8 +317,9 @@ export interface _UppyEventMap { 'preprocess-progress': PreProcessProgressCallback progress: ProgressCallback 'reset-progress': GenericEventCallback - restored: GenericEventCallback + restored: (pluginData: any) => void 'restore-confirmed': GenericEventCallback + 'restore-canceled': GenericEventCallback 'restriction-failed': RestrictionFailedCallback 'resume-all': GenericEventCallback 'retry-all': RetryAllCallback @@ -1713,7 +1714,7 @@ export class Uppy { #updateOnlineStatus = this.updateOnlineStatus.bind(this) - getID(): UppyOptions['id'] { + getID(): string { return this.opts.id } diff --git a/packages/@uppy/core/src/index.ts b/packages/@uppy/core/src/index.ts index ca8462d6c9..083e87a5ef 100644 --- a/packages/@uppy/core/src/index.ts +++ b/packages/@uppy/core/src/index.ts @@ -3,6 +3,7 @@ export { default as Uppy, type UppyEventMap, type State, + type UploadResult, type UnknownPlugin, type UnknownProviderPlugin, type UnknownSearchProviderPlugin, diff --git a/packages/@uppy/golden-retriever/.npmignore b/packages/@uppy/golden-retriever/.npmignore new file mode 100644 index 0000000000..a37256567a --- /dev/null +++ b/packages/@uppy/golden-retriever/.npmignore @@ -0,0 +1,2 @@ + +tsconfig.* diff --git a/packages/@uppy/golden-retriever/src/IndexedDBStore.js b/packages/@uppy/golden-retriever/src/IndexedDBStore.ts similarity index 62% rename from packages/@uppy/golden-retriever/src/IndexedDBStore.js rename to packages/@uppy/golden-retriever/src/IndexedDBStore.ts index 4a33642580..9703f4a25b 100644 --- a/packages/@uppy/golden-retriever/src/IndexedDBStore.js +++ b/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} - */ -function connect (dbName) { - const request = indexedDB.open(dbName, DB_VERSION) +function connect(dbName: string): Promise { + 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} request - * @returns {Promise} - */ -function waitForRequest (request) { +function waitForRequest(request: IDBRequest): Promise { 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} - */ - #ready + #ready: Promise | IDBDatabase - constructor (opts) { + opts: Required + + 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 { 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> { 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(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} */ - async getSize () { + async getSize(): Promise { 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(file: UppyFile): Promise { 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 { 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 { 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((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() diff --git a/packages/@uppy/golden-retriever/src/MetaDataStore.js b/packages/@uppy/golden-retriever/src/MetaDataStore.ts similarity index 59% rename from packages/@uppy/golden-retriever/src/MetaDataStore.js rename to packages/@uppy/golden-retriever/src/MetaDataStore.ts index 87928c9418..59ffb4ac14 100644 --- a/packages/@uppy/golden-retriever/src/MetaDataStore.js +++ b/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 = { + expires: number + metadata: { + currentUploads: UppyState['currentUploads'] + files: UppyState['files'] + pluginData: Record + } +} + /** * 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 && 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( + str: string, +): StoredState | 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 { + opts: Required + + 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['metadata'] | null { const savedState = localStorage.getItem(this.name) if (!savedState) return null - const data = maybeParse(savedState) + const data = maybeParse(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): 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 diff --git a/packages/@uppy/golden-retriever/src/ServiceWorker.js b/packages/@uppy/golden-retriever/src/ServiceWorker.ts similarity index 54% rename from packages/@uppy/golden-retriever/src/ServiceWorker.js rename to packages/@uppy/golden-retriever/src/ServiceWorker.ts index 5a4f3dc1fe..0b07aabdc5 100644 --- a/packages/@uppy/golden-retriever/src/ServiceWorker.js +++ b/packages/@uppy/golden-retriever/src/ServiceWorker.ts @@ -1,38 +1,47 @@ +/* eslint-disable no-restricted-globals */ /* globals clients */ +import type { UppyFile } from '@uppy/utils/lib/UppyFile' + const fileCache = Object.create(null) -function getCache (name) { +function getCache(name: string) { fileCache[name] ??= Object.create(null) return fileCache[name] } self.addEventListener('install', (event) => { - event.waitUntil(Promise.resolve() - .then(() => self.skipWaiting())) + // @ts-expect-error event and self unknown + event.waitUntil(Promise.resolve().then(() => self.skipWaiting())) }) self.addEventListener('activate', (event) => { + // @ts-expect-error event and self unknown event.waitUntil(self.clients.claim()) }) -function sendMessageToAllClients (msg) { +function sendMessageToAllClients(msg: { + type: string + store: string + files: UppyFile[] +}) { + // @ts-expect-error clients unknown clients.matchAll().then((clients) => { - clients.forEach((client) => { + clients.forEach((client: any) => { client.postMessage(msg) }) }) } -function addFile (store, file) { +function addFile(store: string, file: UppyFile) { getCache(store)[file.id] = file.data } -function removeFile (store, fileID) { +function removeFile(store: string, fileID: string) { delete getCache(store)[fileID] } -function getFiles (store) { +function getFiles(store: string) { sendMessageToAllClients({ type: 'uppy/ALL_FILES', store, @@ -52,6 +61,8 @@ self.addEventListener('message', (event) => { getFiles(event.data.store) break default: - throw new Error(`[ServiceWorker] Unsupported event.data.type. Got: ${event?.data?.type}`) + throw new Error( + `[ServiceWorker] Unsupported event.data.type. Got: ${event?.data?.type}`, + ) } }) diff --git a/packages/@uppy/golden-retriever/src/ServiceWorkerStore.js b/packages/@uppy/golden-retriever/src/ServiceWorkerStore.ts similarity index 53% rename from packages/@uppy/golden-retriever/src/ServiceWorkerStore.js rename to packages/@uppy/golden-retriever/src/ServiceWorkerStore.ts index e5c5570d21..370fbc5391 100644 --- a/packages/@uppy/golden-retriever/src/ServiceWorkerStore.js +++ b/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((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 = { + type: string + store: string + file: UppyFile +} + +type ServiceWorkerStoreOptions = { + storeName: string +} + +class ServiceWorkerStore { + #ready: void | Promise + + 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 { 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[]> { 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): Promise { 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 { await this.#ready - navigator.serviceWorker.controller.postMessage({ + navigator.serviceWorker.controller!.postMessage({ type: 'uppy/REMOVE_FILE', store: this.name, fileID, diff --git a/packages/@uppy/golden-retriever/src/cleanup.js b/packages/@uppy/golden-retriever/src/cleanup.js deleted file mode 100644 index 28a862305f..0000000000 --- a/packages/@uppy/golden-retriever/src/cleanup.js +++ /dev/null @@ -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() -} diff --git a/packages/@uppy/golden-retriever/src/cleanup.ts b/packages/@uppy/golden-retriever/src/cleanup.ts new file mode 100644 index 0000000000..fc17e2f962 --- /dev/null +++ b/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() +} diff --git a/packages/@uppy/golden-retriever/src/index.js b/packages/@uppy/golden-retriever/src/index.ts similarity index 55% rename from packages/@uppy/golden-retriever/src/index.js rename to packages/@uppy/golden-retriever/src/index.ts index 6b724edc2c..e1992b8bf0 100644 --- a/packages/@uppy/golden-retriever/src/index.js +++ b/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 { + // TODO: remove this event + 'restore:get-data': (fn: (data: Record) => 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 { 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 - const defaultOptions = { - expires: 24 * 60 * 60 * 1000, // 24 hours - serviceWorker: false, - } + ServiceWorkerStore: ServiceWorkerStore | null - this.opts = { ...defaultOptions, ...opts } + IndexedDBStore: IndexedDBStore + + savedPluginData: Record + + constructor(uppy: Uppy, 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> { + const waitingFiles: Record> = {} 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> { + const uploadingFiles: Record> = {} 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 | Record + > { 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 { + 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): 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 { + await Promise.all( + fileIDs.map( + (id) => + this.ServiceWorkerStore?.delete(id) ?? + this.IndexedDBStore?.delete(id), + ), + ) } - addBlobToStores = (file) => { + addBlobToStores = (file: UppyFile): void => { if (file.isRemote) return if (this.ServiceWorkerStore) { @@ -261,7 +337,7 @@ export default class GoldenRetriever extends BasePlugin { }) } - removeBlobFromStores = (file) => { + removeBlobFromStores = (file: UppyFile): 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): 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): 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 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) diff --git a/packages/@uppy/golden-retriever/tsconfig.build.json b/packages/@uppy/golden-retriever/tsconfig.build.json new file mode 100644 index 0000000000..1b0ca41093 --- /dev/null +++ b/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" + } + ] +} diff --git a/packages/@uppy/golden-retriever/tsconfig.json b/packages/@uppy/golden-retriever/tsconfig.json new file mode 100644 index 0000000000..a76c3b714a --- /dev/null +++ b/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", + }, + ], +} diff --git a/packages/@uppy/status-bar/src/Components.tsx b/packages/@uppy/status-bar/src/Components.tsx index ee5cd83332..11bc73eb28 100644 --- a/packages/@uppy/status-bar/src/Components.tsx +++ b/packages/@uppy/status-bar/src/Components.tsx @@ -15,7 +15,7 @@ const renderDot = (): string => ` ${DOT} ` interface UploadBtnProps { newFiles: number isUploadStarted: boolean - recoveredState: null | State + recoveredState: State['recoveredState'] i18n: I18n uploadState: string isSomeGhost: boolean diff --git a/packages/@uppy/status-bar/src/StatusBarUI.tsx b/packages/@uppy/status-bar/src/StatusBarUI.tsx index b81ff193f7..df3ec2d19b 100644 --- a/packages/@uppy/status-bar/src/StatusBarUI.tsx +++ b/packages/@uppy/status-bar/src/StatusBarUI.tsx @@ -38,7 +38,7 @@ export interface StatusBarUIProps { hidePauseResumeButton?: boolean hideCancelButton?: boolean hideRetryButton?: boolean - recoveredState: null | State + recoveredState: State['recoveredState'] uploadState: (typeof statusBarStates)[keyof typeof statusBarStates] totalProgress: number files: Record> From 977bb99e6bb816fc702f386591ce34f81beae685 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Wed, 13 Mar 2024 16:15:00 +0100 Subject: [PATCH 2/4] Apply feedback --- packages/@uppy/golden-retriever/.npmignore | 1 - .../golden-retriever/src/IndexedDBStore.ts | 2 +- .../{ServiceWorker.ts => ServiceWorker.js} | 29 ++++++------------- 3 files changed, 10 insertions(+), 22 deletions(-) rename packages/@uppy/golden-retriever/src/{ServiceWorker.ts => ServiceWorker.js} (54%) diff --git a/packages/@uppy/golden-retriever/.npmignore b/packages/@uppy/golden-retriever/.npmignore index a37256567a..6c816673f0 100644 --- a/packages/@uppy/golden-retriever/.npmignore +++ b/packages/@uppy/golden-retriever/.npmignore @@ -1,2 +1 @@ - tsconfig.* diff --git a/packages/@uppy/golden-retriever/src/IndexedDBStore.ts b/packages/@uppy/golden-retriever/src/IndexedDBStore.ts index 9703f4a25b..59b691d928 100644 --- a/packages/@uppy/golden-retriever/src/IndexedDBStore.ts +++ b/packages/@uppy/golden-retriever/src/IndexedDBStore.ts @@ -203,7 +203,7 @@ class IndexedDBStore { /** * Save a file in the store. */ - async put(file: UppyFile): Promise { + async put(file: UppyFile): Promise { if (file.data.size > this.opts.maxFileSize) { throw new Error('File is too big to store.') } diff --git a/packages/@uppy/golden-retriever/src/ServiceWorker.ts b/packages/@uppy/golden-retriever/src/ServiceWorker.js similarity index 54% rename from packages/@uppy/golden-retriever/src/ServiceWorker.ts rename to packages/@uppy/golden-retriever/src/ServiceWorker.js index 0b07aabdc5..5a4f3dc1fe 100644 --- a/packages/@uppy/golden-retriever/src/ServiceWorker.ts +++ b/packages/@uppy/golden-retriever/src/ServiceWorker.js @@ -1,47 +1,38 @@ -/* eslint-disable no-restricted-globals */ /* globals clients */ -import type { UppyFile } from '@uppy/utils/lib/UppyFile' - const fileCache = Object.create(null) -function getCache(name: string) { +function getCache (name) { fileCache[name] ??= Object.create(null) return fileCache[name] } self.addEventListener('install', (event) => { - // @ts-expect-error event and self unknown - event.waitUntil(Promise.resolve().then(() => self.skipWaiting())) + event.waitUntil(Promise.resolve() + .then(() => self.skipWaiting())) }) self.addEventListener('activate', (event) => { - // @ts-expect-error event and self unknown event.waitUntil(self.clients.claim()) }) -function sendMessageToAllClients(msg: { - type: string - store: string - files: UppyFile[] -}) { - // @ts-expect-error clients unknown +function sendMessageToAllClients (msg) { clients.matchAll().then((clients) => { - clients.forEach((client: any) => { + clients.forEach((client) => { client.postMessage(msg) }) }) } -function addFile(store: string, file: UppyFile) { +function addFile (store, file) { getCache(store)[file.id] = file.data } -function removeFile(store: string, fileID: string) { +function removeFile (store, fileID) { delete getCache(store)[fileID] } -function getFiles(store: string) { +function getFiles (store) { sendMessageToAllClients({ type: 'uppy/ALL_FILES', store, @@ -61,8 +52,6 @@ self.addEventListener('message', (event) => { getFiles(event.data.store) break default: - throw new Error( - `[ServiceWorker] Unsupported event.data.type. Got: ${event?.data?.type}`, - ) + throw new Error(`[ServiceWorker] Unsupported event.data.type. Got: ${event?.data?.type}`) } }) From d924417f866a941a67efecc5c373d25847f0eb0e Mon Sep 17 00:00:00 2001 From: Merlijn Vos Date: Thu, 14 Mar 2024 12:13:57 +0100 Subject: [PATCH 3/4] Update packages/@uppy/golden-retriever/src/MetaDataStore.ts Co-authored-by: Antoine du Hamel --- packages/@uppy/golden-retriever/src/MetaDataStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@uppy/golden-retriever/src/MetaDataStore.ts b/packages/@uppy/golden-retriever/src/MetaDataStore.ts index 59ffb4ac14..5435964e45 100644 --- a/packages/@uppy/golden-retriever/src/MetaDataStore.ts +++ b/packages/@uppy/golden-retriever/src/MetaDataStore.ts @@ -17,7 +17,7 @@ function findUppyInstances(): string[] { const instances: string[] = [] for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i) - if (key && key.startsWith('uppyState:')) { + if (key?.startsWith('uppyState:')) { instances.push(key.slice('uppyState:'.length)) } } From 1c7fb23c8bbe383a330996b97b4ab64d5f9bb567 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 18 Mar 2024 14:00:46 +0100 Subject: [PATCH 4/4] remove double export --- packages/@uppy/core/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@uppy/core/src/index.ts b/packages/@uppy/core/src/index.ts index 7c579af639..6afec75f44 100644 --- a/packages/@uppy/core/src/index.ts +++ b/packages/@uppy/core/src/index.ts @@ -2,7 +2,6 @@ export { default } from './Uppy.ts' export { default as Uppy, type State, - type UploadResult, type UnknownPlugin, type UnknownProviderPlugin, type UnknownSearchProviderPlugin,