Compare commits

...

10 Commits

6 changed files with 437 additions and 182 deletions

View File

@ -1,5 +1,65 @@
# Changelog
## 2025-02-04 - 3.0.62 - fix(Service Worker)
Refactor and clean up the cache logic in the Service Worker to improve maintainability and handle Safari-specific cache behavior.
- Refactored logic for determining cached domains, enhancing the readability and maintainability of the code.
- Improved handling of CORS settings in caching requests, notably bypassing caching for soft cached domains in Safari to avoid CORS issues.
- Enhanced error response creation for failed resource fetching, maintaining clarity on why and how certain resources were not fetched or cached.
- Revised the structure of the caching logic to ensure consistent behavior across all supported browsers.
## 2025-02-04 - 3.0.61 - fix(ServiceWorkerCacheManager)
Fixed caching mechanism to better support Safari's handling of soft-cached domains.
- Added logic to differentiate between hard and soft cached domains.
- Implemented special handling for soft cached domains on Safari by bypassing caching.
- Ensured appropriate CORS headers are present in cached responses.
- Improved error handling with informative 500 error responses.
- Optimized caching logic to prevent redundant caching and potential issues with locked streams on Safari.
## 2025-02-04 - 3.0.61 - fix(ServiceWorkerCacheManager)
Fixed caching mechanism to better support Safari's handling of soft-cached domains.
- Added logic to differentiate between hard and soft cached domains.
- Implemented special handling for soft cached domains on Safari by bypassing caching.
- Ensured appropriate CORS headers are present in cached responses.
- Improved error handling with informative 500 error responses.
- Optimized caching logic to prevent redundant caching and potential issues with locked streams on Safari.
## 2025-02-04 - 3.0.61 - fix(ServiceWorkerCacheManager)
Fixed caching mechanism to better support Safari's handling of soft-cached domains.
- Added logic to differentiate between hard and soft cached domains.
- Implemented special handling for soft cached domains on Safari by bypassing caching.
- Ensured appropriate CORS headers are present in cached responses.
- Improved error handling with informative 500 error responses.
- Optimized caching logic to prevent redundant caching and potential issues with locked streams on Safari.
## 2025-02-04 - 3.0.60 - fix(cachemanager)
Improve cache management and error handling
- Updated comments for clarity and consistency.
- Enhanced error handling in `fetch` event listener.
- Optimized cache key management and cleanup process.
- Ensured CORS headers are set for cached responses.
- Improved logging for caching operations.
## 2025-02-03 - 3.0.59 - fix(serviceworker)
Fixed CORS and Cache Control handling for Service Worker
- Improved handling of CORS settings for external requests.
- Preserved important headers while excluding caching headers.
- Ensured the presence of CORS headers in cached responses.
- Adjusted Cache-Control headers to prevent browser caching but allow service worker caching.
## 2025-02-03 - 3.0.58 - fix(network-manager)
Refined network management logic for better offline handling.
- Improved logic to handle missing connections more gracefully.
- Added detailed online/offline connection status logging.
- Implemented a check for stale cache with a grace period for offline scenarios.
- Network requests now use optimized retries and timeouts.
## 2025-02-03 - 3.0.57 - fix(updateManager)
Refine cache management for service worker updates.

View File

@ -1,6 +1,6 @@
{
"name": "@api.global/typedserver",
"version": "3.0.57",
"version": "3.0.62",
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
"type": "module",
"exports": {

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@api.global/typedserver',
version: '3.0.57',
version: '3.0.62',
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
}

View File

@ -10,212 +10,279 @@ export class CacheManager {
runtimeCacheName: 'runtime'
};
/**
* Hard cached domains are always attempted to be cached.
* For example, your internal origin, fonts, CDNs, etc.
*/
public hardCachedDomains: string[];
/**
* Soft cached domains will be cached normally on nonSafari browsers,
* but on Safari caching is bypassed to avoid CORS issues.
*/
public softCachedDomains: string[];
constructor(losslessServiceWorkerRefArg: ServiceWorker) {
this.losslessServiceWorkerRef = losslessServiceWorkerRefArg;
// Default hard cached domains.
this.hardCachedDomains = [
this.losslessServiceWorkerRef.serviceWindowRef.location.origin,
'https://unpkg.com',
'https://fonts.googleapis.com',
'https://fonts.gstatic.com'
];
// Default soft cached domains.
this.softCachedDomains = [
'https://assetbroker.'
];
this._setupCache();
}
private _setupCache = () => {
const createMatchRequest = (requestArg: Request) => {
// lets create a matchRequest
let matchRequest: Request;
if (requestArg.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin)) {
// internal request
matchRequest = requestArg;
} else {
matchRequest = new Request(requestArg.url, {
...requestArg.clone(),
mode: 'cors'
});
}
return matchRequest;
};
/**
* creates a 500 response
*/
const create500Response = async (requestArg: Request, responseArg: Response) => {
return new Response(
`
<html>
<head>
<style>
.note {
padding: 10px;
color: #fff;
background: #000;
border-bottom: 1px solid #e4002b;
margin-bottom: 20px;
}
</style>
</head>
<body>
/**
* Returns true if the given URL matches a hard cached domain.
*/
private isHardCached(url: string): boolean {
return this.hardCachedDomains.some(domain => url.includes(domain));
}
/**
* Returns true if the given URL matches a soft cached domain.
*/
private isSoftCached(url: string): boolean {
return this.softCachedDomains.some(domain => url.includes(domain));
}
/**
* Returns true if the given URL should be cached (hard or soft).
*/
private isCacheable(url: string): boolean {
return this.isHardCached(url) || this.isSoftCached(url);
}
/**
* Creates a new Request with appropriate CORS settings.
*/
private createMatchRequest(requestArg: Request): Request {
let matchRequest: Request;
// For internal requests, use the original request.
if (requestArg.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin)) {
matchRequest = requestArg;
} else {
// For external requests, choose the request mode based on whether the URL is soft-cached.
const isSoft = this.isSoftCached(requestArg.url);
// For soft cached domains we use 'no-cors' to avoid CORS errors (at the expense of an opaque response).
const mode: RequestMode = isSoft ? 'no-cors' : 'cors';
matchRequest = new Request(requestArg.url, {
method: requestArg.method,
headers: requestArg.headers,
mode,
credentials: 'same-origin',
redirect: 'follow'
});
}
return matchRequest;
}
/**
* Creates a 500 error response.
*/
private async create500Response(requestArg: Request, responseArg: Response): Promise<Response> {
return new Response(
`
<html>
<head>
<style>
.note {
padding: 10px;
color: #fff;
background: #000;
border-bottom: 1px solid #e4002b;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="note">
<strong>serviceworker running, but status 500</strong><br>
<strong>Service worker running, but status 500</strong><br>
</div>
serviceworker is unable to fetch this request<br>
Here is some info about the request/response pair:<br>
<br>
requestUrl: ${requestArg.url}<br>
responseType: ${responseArg.type}<br>
responseBody: ${await responseArg.clone().text()}<br>
</body>
</html>
Service worker is unable to fetch this request.<br>
Request URL: ${requestArg.url}<br>
Response Type: ${responseArg.type}<br>
Response Body: ${await responseArg.clone().text()}<br>
</body>
</html>
`,
{
headers: {
"Content-Type": "text/html"
},
status: 500
}
);
};
// A list of local resources we always want to be cached.
{
headers: { "Content-Type": "text/html" },
status: 500
}
);
}
/**
* Sets up the fetch event listener and caching logic.
*/
private _setupCache = () => {
this.losslessServiceWorkerRef.serviceWindowRef.addEventListener('fetch', async (fetchEventArg: any) => {
// Lets block scopes we don't want to be passing through the serviceworker
const originalRequest: Request = fetchEventArg.request;
const parsedUrl = new URL(originalRequest.url);
// Exclude specific hosts or paths from being handled by the service worker.
if (
parsedUrl.hostname.includes('paddle.com')
|| parsedUrl.hostname.includes('paypal.com')
|| parsedUrl.hostname.includes('reception.lossless.one')
|| parsedUrl.pathname.startsWith('/socket.io')
|| originalRequest.url.startsWith('https://umami.')
parsedUrl.hostname.includes('paddle.com') ||
parsedUrl.hostname.includes('paypal.com') ||
parsedUrl.hostname.includes('reception.lossless.one') ||
parsedUrl.pathname.startsWith('/socket.io') ||
originalRequest.url.startsWith('https://umami.')
) {
logger.log('note',`serviceworker not active for ${parsedUrl.toString()}`);
logger.log('note', `Service worker not active for ${parsedUrl.toString()}`);
return;
}
// lets continue for the rest
// Create a deferred response.
const done = plugins.smartpromise.defer<Response>();
fetchEventArg.respondWith(done.promise);
if (
(originalRequest.method === 'GET' &&
(originalRequest.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin) &&
!originalRequest.url.includes('/api/') &&
!originalRequest.url.includes('smartserve/reloadcheck'))) ||
originalRequest.url.includes('https://assetbroker.') ||
originalRequest.url.includes('https://unpkg.com') ||
originalRequest.url.includes('https://fonts.googleapis.com') ||
originalRequest.url.includes('https://fonts.gstatic.com')
) {
// lets see if things need to be updated
// not waiting here
this.losslessServiceWorkerRef.updateManager.checkUpdate(this);
// this code block is executed for local requests
const matchRequest = createMatchRequest(originalRequest);
// Only handle GET requests for caching.
if (originalRequest.method === 'GET' && this.isCacheable(originalRequest.url)) {
// Determine if the browser is Safari.
const userAgent = (self.navigator && self.navigator.userAgent) || "";
const isSafari = /Safari/.test(userAgent) && !/Chrome/.test(userAgent);
// For soft cached domains on Safari, bypass caching entirely.
if (this.isSoftCached(originalRequest.url) && isSafari) {
logger.log('info', `Safari detected bypass caching for soft cached domain: ${originalRequest.url}`);
const networkResponse = await fetch(originalRequest).catch(async err => {
return await this.create500Response(originalRequest, new Response(err.message));
});
done.resolve(networkResponse);
return;
}
// Try to serve from cache.
const matchRequest = this.createMatchRequest(originalRequest);
const cachedResponse = await caches.match(matchRequest);
if (cachedResponse) {
logger.log('ok', `CACHED: found cached response for ${matchRequest.url}`);
done.resolve(cachedResponse);
return;
}
// in case there is no cached response
logger.log('info', `NOTYETCACHED: trying to cache ${matchRequest.url}`);
logger.log('info', `NOTYETCACHED: fetching and caching ${matchRequest.url}`);
const newResponse: Response = await fetch(matchRequest).catch(async err => {
return await create500Response(matchRequest, new Response(err.message));
return await this.create500Response(matchRequest, new Response(err.message));
});
// fill cache
// Put a copy of the response in the runtime cache.
if (newResponse.status > 299 || newResponse.type === 'opaque') {
logger.log(
'error',
`NOTCACHED: can't cache response for ${matchRequest.url} due to status ${
newResponse.status
} and type ${newResponse.type}`
);
done.resolve(await create500Response(matchRequest, newResponse));
} else {
const cache = await caches.open(this.usedCacheNames.runtimeCacheName);
const responseToPutToCache = newResponse.clone();
const headers = new Headers();
responseToPutToCache.headers.forEach((value, key) => {
if (
value !== 'Cache-Control'
&& value !== 'cache-control'
&& value !== 'Expires'
&& value !== 'expires'
&& value !== 'Pragma'
&& value !== 'pragma'
) {
headers.set(key, value);
}
});
// Prevent browser caching while allowing service worker caching
headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
headers.set('Pragma', 'no-cache');
headers.set('Expires', '0');
headers.set('Surrogate-Control', 'no-store');
await cache.put(matchRequest, new Response(responseToPutToCache.body, {
...responseToPutToCache,
headers
}));
logger.log(
'ok',
`NOWCACHED: cached response for ${matchRequest.url} for subsequent requests!`
);
done.resolve(newResponse);
// If the response status indicates an error, don't cache.
if (newResponse.status > 299) {
logger.log('error', `NOTCACHED: response for ${matchRequest.url} has status ${newResponse.status}`);
done.resolve(await this.create500Response(matchRequest, newResponse));
return;
}
// Handle opaque responses separately.
if (newResponse.type === 'opaque') {
// For soft-cached domains we expect opaque responses (from no-cors fetches).
if (this.isSoftCached(matchRequest.url)) {
const cache = await caches.open(this.usedCacheNames.runtimeCacheName);
// Cache the opaque response as-is.
await cache.put(matchRequest, newResponse.clone());
logger.log('ok', `NOWCACHED: cached opaque response for ${matchRequest.url}`);
done.resolve(newResponse);
return;
} else {
// If an opaque response comes from a non-soft domain, treat it as an error.
logger.log('error', `NOTCACHED: opaque response received for non-soft domain ${matchRequest.url}`);
done.resolve(await this.create500Response(matchRequest, newResponse));
return;
}
}
// For non-opaque responses, adjust headers and cache.
const cache = await caches.open(this.usedCacheNames.runtimeCacheName);
const responseToCache = newResponse.clone();
const headers = new Headers();
responseToCache.headers.forEach((value, key) => {
// Preserve all headers except caching-related ones.
if (!['Cache-Control', 'cache-control', 'Expires', 'expires', 'Pragma', 'pragma'].includes(key)) {
headers.set(key, value);
}
});
// Ensure necessary CORS headers.
if (!headers.has('Access-Control-Allow-Origin')) {
headers.set('Access-Control-Allow-Origin', '*');
}
if (!headers.has('Access-Control-Allow-Methods')) {
headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
}
if (!headers.has('Access-Control-Allow-Headers')) {
headers.set('Access-Control-Allow-Headers', 'Content-Type');
}
// Prevent browser caching while allowing service worker caching.
headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
headers.set('Pragma', 'no-cache');
headers.set('Expires', '0');
headers.set('Surrogate-Control', 'no-store');
// Read the response body as a blob so we can create a new Response with modified headers.
let newCachedResponse: Response;
try {
const bodyBlob = await responseToCache.blob();
newCachedResponse = new Response(bodyBlob, {
status: responseToCache.status,
statusText: responseToCache.statusText,
headers
});
} catch (err) {
newCachedResponse = newResponse;
}
await cache.put(matchRequest, newCachedResponse);
logger.log('ok', `NOWCACHED: cached response for ${matchRequest.url}`);
done.resolve(newResponse);
} else {
// this code block is executed for remote requests
logger.log(
'ok',
`NOTCACHED: not caching any responses for ${
originalRequest.url
}. Fetching from origin now...`
);
done.resolve(
await fetch(originalRequest).catch(async err => {
return await create500Response(originalRequest, new Response(err.message));
})
);
// For non-cacheable requests, fetch directly from the network.
logger.log('ok', `NOTCACHED: fetching ${originalRequest.url} from origin (non-cacheable)`);
const networkResponse = await fetch(originalRequest).catch(async err => {
return await this.create500Response(originalRequest, new Response(err.message));
});
done.resolve(networkResponse);
}
});
}
};
/**
* update caches
* @param reasonArg
* Cleans all caches.
* Should be run when a new service worker is activated.
* @param reasonArg A reason for the cache cleanup.
*/
/**
* cleans all caches
* should only be run when running a new service worker
* @param reasonArg
*/
public cleanCaches = async (reasonArg = 'no reason given') => {
public cleanCaches = async (reasonArg = 'no reason given'): Promise<void> => {
logger.log('info', `MAJOR CACHEEVENT: cleaning caches now! Reason: ${reasonArg}`);
const cacheNames = await caches.keys();
const deletePromises = cacheNames.map(cacheToDelete => {
const deletePromise = caches.delete(cacheToDelete);
deletePromise.then(() => {
logger.log('ok', `Deleted cache ${cacheToDelete}`);
});
return deletePromise;
const deletePromise = caches.delete(cacheToDelete);
deletePromise.then(() => {
logger.log('ok', `Deleted cache ${cacheToDelete}`);
});
return deletePromise;
});
await Promise.all(deletePromises);
}
};
/**
* revalidate cache
* Revalidates the runtime cache.
*/
public async revalidateCache() {
public async revalidateCache(): Promise<void> {
const runtimeCache = await caches.open(this.usedCacheNames.runtimeCacheName);
const cacheKeys = await runtimeCache.keys();
for (const requestArg of cacheKeys) {
const cachedResponse = runtimeCache.match(requestArg);
// lets get a new response for comparison
const clonedRequest = requestArg.clone();
const response = await plugins.smartpromise.timeoutWrap(fetch(clonedRequest), 5000); // Increased timeout for better mobile compatibility
const response = await plugins.smartpromise.timeoutWrap(fetch(clonedRequest), 5000);
if (response && response.status >= 200 && response.status < 300) {
await runtimeCache.delete(requestArg);
await runtimeCache.put(requestArg, response);

View File

@ -1,18 +1,37 @@
import * as plugins from './plugins.js';
import { ServiceWorker } from './classes.serviceworker.js';
import { logger } from './logging.js';
export class NetworkManager {
public serviceWorkerRef: ServiceWorker;
public webRequest: plugins.webrequest.WebRequest;
private isOffline: boolean = false;
private lastOnlineCheck: number = 0;
private readonly ONLINE_CHECK_INTERVAL = 30000; // 30 seconds
public previousState: string;
constructor(serviceWorkerRefArg: ServiceWorker) {
this.serviceWorkerRef = serviceWorkerRefArg;
this.webRequest = new plugins.webrequest.WebRequest();
// Listen for connection changes
this.getConnection()?.addEventListener('change', () => {
this.updateConnectionStatus();
});
// Listen for online/offline events
self.addEventListener('online', () => {
this.isOffline = false;
logger.log('info', 'Device is now online');
this.updateConnectionStatus();
});
self.addEventListener('offline', () => {
this.isOffline = true;
logger.log('warn', 'Device is now offline');
this.updateConnectionStatus();
});
}
/**
@ -28,6 +47,81 @@ export class NetworkManager {
}
public updateConnectionStatus() {
console.log(`Connection type changed from ${this.previousState} to ${this.getEffectiveType()}`);
const currentType = this.getEffectiveType();
logger.log('info', `Connection type changed from ${this.previousState} to ${currentType}`);
this.previousState = currentType;
}
/**
* Checks if the device is currently online by attempting to contact the server
* @returns Promise<boolean> true if online, false if offline
*/
public async checkOnlineStatus(): Promise<boolean> {
const now = Date.now();
// Only check if enough time has passed since last check
if (now - this.lastOnlineCheck < this.ONLINE_CHECK_INTERVAL) {
return !this.isOffline;
}
try {
const response = await fetch('/sw-typedrequest', {
method: 'HEAD',
cache: 'no-cache'
});
this.isOffline = false;
this.lastOnlineCheck = now;
return true;
} catch (error) {
this.isOffline = true;
this.lastOnlineCheck = now;
logger.log('warn', 'Device appears to be offline');
return false;
}
}
/**
* Makes a network request with offline handling
* @param request The request to make
* @param options Additional options
* @returns Promise<Response>
*/
public async makeRequest<T>(request: Request | string, options: {
timeoutMs?: number;
retries?: number;
backoffMs?: number;
} = {}): Promise<Response> {
const {
timeoutMs = 5000,
retries = 1,
backoffMs = 1000
} = options;
let lastError: Error;
for (let i = 0; i <= retries; i++) {
try {
const isOnline = await this.checkOnlineStatus();
if (!isOnline) {
throw new Error('Device is offline');
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const response = await fetch(request, {
...typeof request === 'string' ? {} : request,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
lastError = error;
if (i < retries) {
await new Promise(resolve => setTimeout(resolve, backoffMs * (i + 1)));
}
}
}
throw lastError;
}
}

View File

@ -19,6 +19,7 @@ export class UpdateManager {
*/
private readonly MAX_CACHE_AGE = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
private readonly MIN_CHECK_INTERVAL = 100000; // 100 seconds in milliseconds
private readonly OFFLINE_GRACE_PERIOD = 7 * 24 * 60 * 60 * 1000; // 7 days grace period when offline
private lastCacheTimestamp: number = 0;
public async checkUpdate(cacheManager: CacheManager): Promise<boolean> {
@ -44,11 +45,23 @@ export class UpdateManager {
const millisSinceLastCheck = now - this.lastUpdateCheck;
const cacheAge = now - this.lastCacheTimestamp;
// Force update if cache is too old
// Check if we need to handle stale cache
if (cacheAge > this.MAX_CACHE_AGE) {
logger.log('info', `Cache is older than ${this.MAX_CACHE_AGE}ms, forcing update...`);
await this.forceUpdate(cacheManager);
return true;
const isOnline = await this.serviceworkerRef.networkManager.checkOnlineStatus();
if (isOnline) {
logger.log('info', `Cache is older than ${this.MAX_CACHE_AGE}ms, forcing update...`);
await this.forceUpdate(cacheManager);
return true;
} else if (cacheAge > this.OFFLINE_GRACE_PERIOD) {
// If we're offline and beyond grace period, warn but continue serving cached content
logger.log('warn', `Cache is stale and device is offline. Cache age: ${cacheAge}ms. Using cached content with warning.`);
// We could potentially show a warning to the user here
return false;
} else {
logger.log('info', `Cache is stale but device is offline. Within grace period. Using cached content.`);
return false;
}
}
// Regular update check interval
@ -85,11 +98,24 @@ export class UpdateManager {
* gets the apphash from the server
*/
public async getVersionInfoFromServer() {
const getAppHashRequest = new plugins.typedrequest.TypedRequest<
interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo
>('/sw-typedrequest', 'serviceworker_versionInfo');
const result = await getAppHashRequest.fire({});
return result;
try {
const getAppHashRequest = new plugins.typedrequest.TypedRequest<
interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo
>('/sw-typedrequest', 'serviceworker_versionInfo');
// Use networkManager for the request with retries and timeout
const response = await this.serviceworkerRef.networkManager.makeRequest('/sw-typedrequest', {
timeoutMs: 5000,
retries: 2,
backoffMs: 1000
});
const result = await response.json();
return result;
} catch (error) {
logger.log('warn', `Failed to get version info from server: ${error.message}`);
throw error;
}
}
// tasks
@ -97,14 +123,22 @@ export class UpdateManager {
* this task is executed once we know that there is a new version available
*/
private async forceUpdate(cacheManager: CacheManager) {
logger.log('info', 'Forcing cache update due to staleness');
await this.serviceworkerRef.cacheManager.cleanCaches('Cache is stale, forcing update.');
const currentVersionInfo = await this.getVersionInfoFromServer();
this.lastVersionInfo = currentVersionInfo;
await this.serviceworkerRef.store.set('versionInfo', this.lastVersionInfo);
this.lastCacheTimestamp = Date.now();
await this.serviceworkerRef.store.set('cacheTimestamp', this.lastCacheTimestamp);
await this.serviceworkerRef.leleServiceWorkerBackend.triggerReloadAll();
try {
logger.log('info', 'Forcing cache update due to staleness');
const currentVersionInfo = await this.getVersionInfoFromServer();
// Only proceed with cache cleaning if we successfully got new version info
await this.serviceworkerRef.cacheManager.cleanCaches('Cache is stale, forcing update.');
this.lastVersionInfo = currentVersionInfo;
await this.serviceworkerRef.store.set('versionInfo', this.lastVersionInfo);
this.lastCacheTimestamp = Date.now();
await this.serviceworkerRef.store.set('cacheTimestamp', this.lastCacheTimestamp);
await this.serviceworkerRef.leleServiceWorkerBackend.triggerReloadAll();
} catch (error) {
logger.log('error', `Failed to force update: ${error.message}. Keeping existing cache.`);
// If update fails, we'll keep using the existing cache
throw error;
}
}
public performAsyncUpdateDebouncedTask = new plugins.taskbuffer.TaskDebounced({