// ==UserScript==
// @name YouTube Unhold IndexedDB
// @name:en YouTube Unhold IndexedDB
// @name:ja YouTube Unhold IndexedDB
// @name:zh-TW YouTube Unhold IndexedDB
// @name:zh-CN YouTube Unhold IndexedDB
// @namespace http://tampermonkey.net/
// @version 2022.12.27
// @license MIT License
// @description Release YouTube's used IndexDBs to make background tabs able to sleep
// @description:en Release YouTube's used IndexDBs to make background tabs able to sleep
// @description:ja YouTube の 使用済みIndexDB を解放して、バックグラウンドページを休止状態になるように
// @description:zh-TW 釋放 YouTube 用過的 IndexDBs 讓後台頁面能進入休眠
// @description:zh-CN 释放 YouTube 用过的 IndexDBs 让后台页面能进入休眠
// @author CY Fung
// @match https://www.youtube.com/*
// @match https://www.youtube.com/embed/*
// @match https://www.youtube-nocookie.com/embed/*
// @match https://www.youtube.com/live_chat*
// @match https://www.youtube.com/live_chat_replay*
// @match https://music.youtube.com/*
// @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
// @icon https://raw.githubusercontent.com/cyfung1031/userscript-supports/main/icons/youtube-unlock-indexedDB.png
// @supportURL https://github.com/cyfung1031/userscript-supports
// @run-at document-start
// @grant none
// @unwrap
// @allFrames
// @inject-into page
// ==/UserScript==
/* jshint esversion:8 */
const DEBUG_LOG = false;
const DB_NAME_FOR_TESTING = 'testdb-Q4IOpq0p'
let runCount = 0;
const isSupported = (function (console, consoleX) {
'use strict';
let [window] = new Function('return [window];')(); // real window object
const isSupported = (((window || 0).indexedDB || 0).constructor || 0).name === 'IDBFactory' && typeof requestIdleCallback === 'function'
if (isSupported) {
const addEventListenerKey = Symbol();
const removeEventListenerKey = Symbol();
const openKey = Symbol();
const funcHooks = new WeakMap();
let openCount = 0;
const msgStore = [];
const message = (message) => {
msgStore.push(message);
if (openCount === 0 && msgStore.length > 0) {
setTimeout(() => {
if (openCount === 0 && msgStore.length > 0) {
let messages = [...msgStore]
msgStore.length = 0
messages.sort((a, b) => a.databaseId.localeCompare(b.databaseId) || a.time - b.time)
consoleX.dir(messages)
}
}, 300)
}
};
const mTime = Date.now()
function releaseOnIdle(db, databaseId, eventType, event_type) {
setTimeout(() => {
requestIdleCallback(() => {
Promise.resolve(db).then((db) => {
DEBUG_LOG && console.log(db, databaseId, eventType, event_type)
db.close();
db = null;
openCount--
message({ databaseId: databaseId, action: 'close', time: Date.now() })
runCount++
if (runCount > 1e9) runCount = 0
}).catch(consoleX.warn)
db = null
}, { timeout: 300 })
}, 300)
}
function makeHandler(handler, databaseId, eventType) {
return function (event) {
DEBUG_LOG && console.log(32, 'addEventListener', databaseId, eventType, event.type)
handler.call(this, arguments)
releaseOnIdle(event.target.result, databaseId, eventType, event.type)
DEBUG_LOG && console.log(441, 'addEventListener', databaseId, eventType, event.type)
}
}
function makeAddEventListener(databaseId) {
return function (eventType, handler) {
const addEventListener = this[addEventListenerKey]
if (arguments.length !== 2) return addEventListener.call(this, ...arguments)
if (eventType === 'error' || eventType === 'success') {
DEBUG_LOG && console.log(31, databaseId, eventType)
let gx = funcHooks.get(handler)
if (!gx) {
gx = makeHandler(handler, databaseId, eventType) // databaseId and eventType are just for logging; not reliable
funcHooks.set(handler, gx)
}
return addEventListener.call(this, eventType, gx)
}
return addEventListener.call(this, ...arguments)
}
}
function makeRemoveEventListener(databaseId) {
return function (eventType, handler) {
const removeEventListener = this[removeEventListenerKey]
if (arguments.length !== 2) return removeEventListener.call(this, ...arguments)
if (eventType === 'error' || eventType === 'success') {
let gx = funcHooks.get(handler)
DEBUG_LOG && console.log(30, 'removeEventListener', databaseId, eventType)
let ret = removeEventListener.call(this, eventType, gx || handler)
DEBUG_LOG && console.log(442, 'removeEventListener', databaseId, eventType)
return ret
}
return removeEventListener.call(this, ...arguments);
}
}
function makeOpen() {
return function (databaseId) {
let request = this[openKey](databaseId); // IDBRequest
request[addEventListenerKey] = request.addEventListener;
request.addEventListener = makeAddEventListener(databaseId);
request[removeEventListenerKey] = request.removeEventListener;
request.removeEventListener = makeRemoveEventListener(databaseId);
openCount++
message({ databaseId: databaseId, action: 'open', time: Date.now() })
return request;
}
}
window.indexedDB.constructor.prototype[openKey] = window.indexedDB.constructor.prototype.open;
window.indexedDB.constructor.prototype.open = makeOpen();
}
console.log(22)
return isSupported
})(DEBUG_LOG ? console : Object.assign({}, console, { log: function () { } }), console);
isSupported && (function () {
let request = indexedDB.open(DB_NAME_FOR_TESTING);
let mi = 0;
let px = function () {
mi += 1000;
};
request.addEventListener('success', px);
request.addEventListener('success', function () {
mi += 101;
});
request.addEventListener('error', px);
request.addEventListener('error', function () {
mi += 201;
});
request.removeEventListener('success', px);
request.removeEventListener('error', px);
indexedDB.deleteDatabase(DB_NAME_FOR_TESTING);
setTimeout(() => {
requestIdleCallback(() => {
setTimeout(() => {
Promise.resolve(0).then(() => {
if ((mi === 101 || mi === 201) && runCount >= 1) {
console.log(`%cYouTube Unhold IndexDB - %cInjection Success ${mi} ${runCount}`, 'background: #222; color: #fff', 'background: #222; color: #bada55');
} else {
console.log(`%cYouTube Unhold IndexDB - %cInjection Failure ${mi} ${runCount}`, 'background: #222; color: #fff', 'background: #222; color: #da5a2f');
}
}).catch(console.warn)
}, 100)
}, { timeout: 300 })
}, 300)
})();