Downloads Decorators
The Downloads decorators allow you to easily respond to download events in your extension service. These decorators provide a clean way to monitor and react to downloads being created, changed, or erased.
Method Decorators
onDownloadCreated
This decorator handles events that fire when a download begins.
import { onDownloadCreated, InjectableService } from 'deco-ext';
@InjectableService()
class DownloadMonitor {
@onDownloadCreated()
handleNewDownload(downloadItem: browser.Downloads.DownloadItem) {
console.log(`New download started: ${downloadItem.filename}`);
console.log(`Download ID: ${downloadItem.id}`);
console.log(`URL: ${downloadItem.url}`);
console.log(`File size: ${downloadItem.fileSize} bytes`);
}
}
With parameter decorator:
import { onDownloadCreated, downloadItem, InjectableService } from 'deco-ext';
@InjectableService()
class DownloadMonitor {
@onDownloadCreated()
handleNewDownload(
@downloadItem('id') id: number,
@downloadItem('filename') filename: string,
@downloadItem('url') url: string,
@downloadItem('mime') mimeType: string
) {
console.log(`Download #${id} started: ${filename}`);
console.log(`From URL: ${url}`);
console.log(`MIME type: ${mimeType}`);
}
}
onDownloadChanged
This decorator handles events that fire when a download is updated.
import { onDownloadChanged, InjectableService } from 'deco-ext';
@InjectableService()
class DownloadMonitor {
@onDownloadChanged()
handleDownloadChange(downloadDelta: browser.Downloads.OnChangedDownloadDeltaType) {
console.log(`Download #${downloadDelta.id} was changed`);
if (downloadDelta.state) {
console.log(`State changed to: ${downloadDelta.state.current}`);
}
if (downloadDelta.paused) {
console.log(`Paused state changed to: ${downloadDelta.paused.current}`);
}
if (downloadDelta.error) {
console.log(`Error: ${downloadDelta.error.current}`);
}
}
}
With parameter decorator:
import { onDownloadChanged, downloadDelta, InjectableService } from 'deco-ext';
@InjectableService()
class DownloadMonitor {
@onDownloadChanged()
handleDownloadChange(
@downloadDelta('id') id: number,
@downloadDelta('state') state: { current: string; previous: string } | undefined,
@downloadDelta('totalBytes') bytes: { current: number; previous: number } | undefined
) {
console.log(`Download #${id} was changed`);
if (state) {
console.log(`State changed from ${state.previous} to ${state.current}`);
}
if (bytes) {
console.log(`Total bytes changed from ${bytes.previous} to ${bytes.current}`);
}
}
}
onDownloadErased
This decorator handles events that fire when a download is erased from history.
import { onDownloadErased, InjectableService } from 'deco-ext';
@InjectableService()
class DownloadMonitor {
@onDownloadErased()
handleDownloadErasure(downloadId: number) {
console.log(`Download #${downloadId} was erased from history`);
// Clean up any references to this download in your extension
this.removeDownloadReference(downloadId);
}
private removeDownloadReference(id: number) {
// Custom cleanup logic
}
}
Filtering Events
All download decorators support an optional filter parameter that allows you to conditionally handle events. The filter function receives the same arguments as your decorated method and should return true to proceed with handling the event, or false to skip it.
⚡ Performance Benefit: When a filter returns false (or Promise<false>), the decorated class instance is not created at all, significantly reducing memory usage and improving performance by avoiding unnecessary object instantiation and initialization.
🔒 Scope Limitation: Filter functions execute before class instantiation, so they cannot access instance properties or methods (this is not available). Use module-level variables, closures, or static data for filtering logic.
Basic Filtering Examples
import { onDownloadCreated, onDownloadChanged, InjectableService } from 'deco-ext';
@InjectableService()
class DownloadFilterService {
// Only handle downloads of specific file types
@onDownloadCreated({
filter: (downloadItem) => downloadItem.filename.endsWith('.pdf') || downloadItem.filename.endsWith('.doc')
})
handleDocumentDownloads(downloadItem: browser.Downloads.DownloadItem) {
console.log(`Document download started: ${downloadItem.filename}`);
}
// Only handle large downloads
@onDownloadCreated({
filter: (downloadItem) => downloadItem.totalBytes && downloadItem.totalBytes > 10 * 1024 * 1024 // > 10MB
})
handleLargeDownloads(downloadItem: browser.Downloads.DownloadItem) {
console.log(`Large download started: ${downloadItem.filename} (${downloadItem.totalBytes} bytes)`);
}
// Only handle download completions
@onDownloadChanged({
filter: (downloadDelta) => downloadDelta.state?.current === 'complete'
})
handleDownloadCompletions(downloadDelta: browser.Downloads.OnChangedDownloadDeltaType) {
console.log(`Download #${downloadDelta.id} completed successfully`);
}
}
Advanced Filtering Examples
import { onDownloadCreated, onDownloadChanged, onDownloadErased, InjectableService } from 'deco-ext';
// Module-level tracking (accessible to filters)
const trackedDownloads = new Set<number>();
const trustedDomains = ['github.com', 'stackoverflow.com'];
const largeFileThreshold = 10 * 1024 * 1024; // 10MB
@InjectableService()
class AdvancedDownloadService {
// Filter downloads from specific domains
@onDownloadCreated({
filter: (downloadItem) => {
try {
const url = new URL(downloadItem.url);
return trustedDomains.includes(url.hostname);
} catch {
return false;
}
}
})
handleTrustedSiteDownloads(downloadItem: browser.Downloads.DownloadItem) {
console.log(`Download from trusted site: ${downloadItem.url}`);
// Add to module-level tracking
trackedDownloads.add(downloadItem.id);
}
// Filter download changes with error handling (using module-level tracking)
@onDownloadChanged({
filter: async (downloadDelta) => {
// Only handle downloads we're tracking and that had errors
return trackedDownloads.has(downloadDelta.id) &&
downloadDelta.error?.current !== undefined;
}
})
handleTrackedDownloadErrors(downloadDelta: browser.Downloads.OnChangedDownloadDeltaType) {
console.log(`Error in tracked download #${downloadDelta.id}: ${downloadDelta.error?.current}`);
}
// Only handle erasure of recently completed downloads
@onDownloadErased({
filter: async (downloadId) => {
try {
const [downloadItem] = await browser.downloads.search({ id: downloadId });
if (!downloadItem) return false;
// Only if completed within last hour
const oneHourAgo = Date.now() - (60 * 60 * 1000);
return downloadItem.endTime && new Date(downloadItem.endTime).getTime() > oneHourAgo;
} catch {
return false;
}
}
})
handleRecentDownloadErasure(downloadId: number) {
console.log(`Recently completed download #${downloadId} was erased`);
// Remove from module-level tracking
trackedDownloads.delete(downloadId);
}
}
Filter with Parameter Decorators
Filters work seamlessly with parameter decorators:
import { onDownloadChanged, downloadDelta, InjectableService } from 'deco-ext';
@InjectableService()
class DownloadProgressService {
@onDownloadChanged({
filter: (downloadDelta) => downloadDelta.percentComplete !== undefined // Only progress updates
})
trackDownloadProgress(
@downloadDelta('id') id: number,
@downloadDelta('percentComplete') progress: { current: number; previous: number } | undefined
) {
if (progress) {
console.log(`Download #${id} progress: ${progress.current}%`);
// Notify user at certain milestones
if (progress.current >= 100) {
this.notifyDownloadComplete(id);
} else if (progress.current >= 50 && progress.previous < 50) {
this.notifyDownloadHalfway(id);
}
}
}
private notifyDownloadComplete(id: number) {
console.log(`Download #${id} finished!`);
}
private notifyDownloadHalfway(id: number) {
console.log(`Download #${id} is halfway complete`);
}
}
Parameter Decorators
downloadItem
Used with onDownloadCreated to extract specific properties from the download item:
import { onDownloadCreated, downloadItem, InjectableService } from 'deco-ext';
@InjectableService()
class DownloadLogger {
@onDownloadCreated()
logNewDownload(
@downloadItem('id') id: number,
@downloadItem('filename') filename: string,
@downloadItem('url') url: string,
@downloadItem('mime') mimeType: string,
@downloadItem('startTime') startTime: string,
@downloadItem('totalBytes') totalBytes: number
) {
console.log(`Download #${id}: ${filename}`);
console.log(`Started at: ${startTime}`);
console.log(`Source: ${url}`);
console.log(`MIME type: ${mimeType}`);
console.log(`Size: ${totalBytes} bytes`);
}
}
downloadDelta
Used with onDownloadChanged to extract specific properties from the download delta object:
import { onDownloadChanged, downloadDelta, InjectableService } from 'deco-ext';
@InjectableService()
class DownloadProgressTracker {
@onDownloadChanged()
trackProgress(
@downloadDelta('id') id: number,
@downloadDelta('state') state: { current: string; previous: string } | undefined,
@downloadDelta('percentComplete') percent: { current: number; previous: number } | undefined
) {
if (state && state.current === 'in_progress') {
if (percent) {
console.log(`Download #${id} progress: ${percent.current}%`);
}
} else if (state && state.current === 'complete') {
console.log(`Download #${id} finished!`);
}
}
}
Download Objects
DownloadItem
The DownloadItem object provided to onDownloadCreated typically includes:
id: The identifier for the downloadurl: The URL of the downloaded filefilename: The filename of the downloaded filemime: The MIME type of the downloaded filestartTime: The time when the download beganendTime: The time when the download ended (if it's complete)state: The download state (e.g., "in_progress", "interrupted", "complete")fileSize: The size of the file in bytesexists: Whether the downloaded file still existsbyExtensionId: The ID of the extension that initiated the download (if applicable)byExtensionName: The name of the extension that initiated the download (if applicable)
OnChangedDownloadDeltaType
The OnChangedDownloadDeltaType object provided to onDownloadChanged includes:
id: The identifier for the download- Various optional properties representing changes, each containing
currentandpreviousvalues:state: The download statepaused: Whether the download is pausederror: Any error that occurredtotalBytes: Total bytes of the downloadfileSize: Size of the file in bytesexists: Whether the file existsfilename: The filenamemime: The MIME typeestimatedEndTime: The estimated completion timedangerType: Any security risks associated with the fileurl: The download URLfinalUrl: The final URL after redirects
Implementation Details
These decorators use a singleton pattern to ensure only one event listener is registered per event type, and then route events to all decorated methods. When a download event occurs:
- The event is received by the single registered browser API listener
- The event data is passed to all registered method handlers
- For each handler:
- The class instance is resolved from the dependency injection container
- If the class has an
initmethod, it's called before handling the event - If parameter decorators are used, the event data is transformed accordingly
- The method is called with the appropriate parameters
The decorators can only be used on methods within classes that have been decorated with the InjectableService decorator from deco-ext.