make-bilibili-great-than-ever-before
Version:
A fork of @kookxiang's userscript "Make Bilibili Great Again", but with many experimental features
5,075 lines • 201 kB
JavaScript
// ==UserScript==
// @name Make Bilibili Great Than Ever Before
// @description A fork of @kookxiang's userscript "Make Bilibili Great Again", but with many experimental features
// @namespace https://skk.moe
// @run-at document-start
// @match https://www.bilibili.com/*
// @match https://t.bilibili.com/*
// @match https://live.bilibili.com/*
// @match https://space.bilibili.com/*
// @updateURL https://unpkg.com/make-bilibili-great-than-ever-before@latest/dist/make-bilibili-great-than-ever-before.meta.js
// @downloadURL https://unpkg.com/make-bilibili-great-than-ever-before@latest/dist/make-bilibili-great-than-ever-before.user.js
// @version 1.8.4
// @author SukkaW <https://skk.moe>
// @grant unsafeWindow
// @grant GM.notification
// @grant GM_getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.listValues
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// ==/UserScript==
(function(){'use strict';const o$2=/* @__NO_SIDE_EFFECTS__ */()=>{},r$1=/* @__NO_SIDE_EFFECTS__ */()=>true,e$4=/* @__NO_SIDE_EFFECTS__ */()=>false;const consoleLog = unsafeWindow.console.log;
const consoleError = unsafeWindow.console.error;
const consoleWarn = unsafeWindow.console.warn;
const consoleInfo = unsafeWindow.console.info;
const consoleDebug = unsafeWindow.console.debug;
const consoleTrace = unsafeWindow.console.trace;
const consoleGroup = unsafeWindow.console.group;
const consoleGroupCollapsed = unsafeWindow.console.groupCollapsed;
const consoleGroupEnd = unsafeWindow.console.groupEnd;
const logger = {
log: consoleLog.bind(console, '[make-bilibili-great-than-ever-before]'),
error: consoleError.bind(console, '[make-bilibili-great-than-ever-before]'),
warn: consoleWarn.bind(console, '[make-bilibili-great-than-ever-before]'),
info: consoleInfo.bind(console, '[make-bilibili-great-than-ever-before]'),
debug: consoleDebug.bind(console, '[make-bilibili-great-than-ever-before]'),
trace (...args) {
consoleGroupCollapsed.bind(console, '[make-bilibili-great-than-ever-before]')(...args);
consoleTrace(...args);
consoleGroupEnd();
},
group: consoleGroup.bind(console, '[make-bilibili-great-than-ever-before]'),
groupCollapsed: consoleGroupCollapsed.bind(console, '[make-bilibili-great-than-ever-before]'),
groupEnd: consoleGroupEnd.bind(console)
};function getUrlFromRequest(request) {
if (typeof request === 'string') return request;
if ('href' in request) return request.href;
if ('url' in request) return request.url;
logger.error('Invalid requestInfo', request);
return null;
}function createMockClass(className, instanceMethods = {}, staticMethods = {}) {
const fakeClassInstance = new Proxy(o$2, {
get (target, prop) {
if (prop in instanceMethods) return instanceMethods[prop];
return (...args)=>{
logger.log(`(new ${className})[${String(prop)}] called with arguments:`, args);
};
}
});
return new Proxy(class {
}, {
construct () {
return fakeClassInstance;
},
get (target, prop) {
if (prop in staticMethods) return staticMethods[prop];
return (...args)=>{
logger.log(`window.${className}[${String(prop)}] called with arguments:`, args);
};
}
});
}function defineReadonlyProperty(target, key, value, enumerable = true) {
Object.defineProperty(target, key, {
get () {
return value;
},
set: o$2,
configurable: false,
enumerable
});
}const n$1=new Set([".","?","*","+","^","$","|","(",")","{","}","[","]","\\"]);function e$3(t,o=false){const r={},i=(t,n)=>{let e,o=r;for(let r=0,i=t.length;r<i;++r)(e=t.charAt(r))in o||(o[e]=n?{"":""}:{}),o=o[e];o[""]="";};for(let n=0,e=t.length;n<e;++n)i(t[n],o);const c=()=>(function t(e){let o,r,i=false,c=false;const l=[],s=[];for(const o in e){if(!o){i=true;continue}((r=t(e[o]))?l:s).push(("-"===o?"\\x2d":n$1.has(o)?"\\"+o:o)+r);}return i&&null==r?"":(c=!l.length,s.length&&l.push(s[1]?"["+s.join("")+"]":s[0]),o=l[1]?"(?:"+l.join("|")+")":l[0],i&&(o=c?o+"?":"(?:"+o+")?"),o||"")})(r);return {tree:r,add:i,toString:c,toRe:()=>new RegExp((o?"^":"")+c())}}function o$1(n,r=false){if(0===n.length)return e$4;const i=e$3(n,r).toRe();return i.test.bind(i)}const shouldDefuseUrl = o$1([
'data.bilibili.com',
'cm.bilibili.com',
'api.bilibili.com/x/internal/gaia-gateway/ExClimbWuzhi'
]);
const defuseSpyware = {
name: 'defuse-spyware',
description: '禁用叔叔日志上报和用户跟踪的无限请求风暴',
any ({ onBeforeFetch, onXhrOpen }) {
defineReadonlyProperty(unsafeWindow.navigator, 'sendBeacon', r$1);
const SentryHub = createMockClass('SentryHub');
const fakeSentry = {
SDK_NAME: 'sentry.javascript.browser',
SDK_VERSION: '0.0.1145141919810',
BrowserClient: createMockClass('Sentry.BrowserClient'),
Hub: SentryHub,
Integrations: {
Vue: createMockClass('Sentry.Integrations.Vue'),
GlobalHandlers: createMockClass('Sentry.Integrations.GlobalHandlers'),
InboundFilters: createMockClass('Sentry.Integrations.InboundFilters')
},
init: o$2,
configureScope: o$2,
getCurrentHub: ()=>new SentryHub(),
setContext: o$2,
setExtra: o$2,
setExtras: o$2,
setTag: o$2,
setTags: o$2,
setUser: o$2,
wrap: o$2
};
defineReadonlyProperty(unsafeWindow, 'Sentry', fakeSentry);
defineReadonlyProperty(unsafeWindow, 'MReporter', createMockClass('MReporter'));
defineReadonlyProperty(unsafeWindow, 'ReporterPb', createMockClass('ReporterPb'));
defineReadonlyProperty(unsafeWindow, '__biliUserFp__', {
init: o$2,
queryUserLog () {
return [];
}
});
defineReadonlyProperty(unsafeWindow, '__USER_FP_CONFIG__', undefined);
defineReadonlyProperty(unsafeWindow, '__MIRROR_CONFIG__', undefined);
onBeforeFetch((fetchArgs)=>{
const url = getUrlFromRequest(fetchArgs[0]);
if (typeof url === 'string' && shouldDefuseUrl(url)) return new Response();
return fetchArgs;
});
onXhrOpen((args)=>{
let url = args[1];
if (typeof url !== 'string') url = url.href;
if (shouldDefuseUrl(url)) return null;
return args;
});
}
};class ListNode {
timestamp;
next = null;
prev = null;
constructor(timestamp){
this.timestamp = timestamp;
}
}
class ErrorCounter {
timeWindow;
head = null;
tail = null;
intervalId;
$size = 0;
constructor(timeWindow = 10000){
this.timeWindow = timeWindow;
this.intervalId = self.setInterval(()=>this.cleanup(), 1000);
}
recordError() {
const now = Date.now();
const newNode = new ListNode(now);
if (this.tail) {
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
} else {
this.head = newNode;
this.tail = newNode;
}
this.$size++;
}
getErrorCount() {
this.cleanup();
return this.$size;
// let count = 0;
// let current = this.head;
// while (current) {
// count++;
// current = current.next;
// }
// return count;
}
cleanup() {
const now = Date.now();
while(this.head && now - this.head.timestamp > this.timeWindow){
this.head = this.head.next;
if (this.head) this.head.prev = null;
else this.tail = null;
this.$size--;
}
}
stop() {
clearInterval(this.intervalId);
}
}/**
* @example import { tagged as html } from 'foxts/tagged';
*/function e$2(r,...t){return r.reduce((e,r,n)=>e+r+(t[n]??""),"")}function flru (max) {
var num, curr, prev;
var limit = max || 1;
function keep(key, value) {
if (++num > limit) {
prev = curr;
reset(1);
++num;
}
curr[key] = value;
}
function reset(isPartial) {
num = 0;
curr = Object.create(null);
isPartial || (prev=Object.create(null));
}
reset();
return {
clear: reset,
has: function (key) {
return curr[key] !== void 0 || prev[key] !== void 0;
},
get: function (key) {
var val = curr[key];
if (val !== void 0) return val;
if ((val=prev[key]) !== void 0) {
keep(key, val);
return val;
}
},
set: function (key, value) {
if (curr[key] !== void 0) {
curr[key] = value;
} else {
keep(key, value);
}
}
};
}// const mcdnRegexp = /[\dxy]+\.mcdn\.bilivideo\.cn:\d+/;
const qualityRegexp = /(live-bvc\/\d+\/live_\d+_\d+)_\w+/;
const hevcRegexp = /(\d+)_(?:mini|pro)hevc/g;
const smtcdnsRegexp = /[\w.]+\.smtcdns.net\/([\w-]+\.bilivideo.com\/)/;
const liveCdnUrlKwFilter = o$1([
'.bilivideo.',
'.m3u8',
'.m4s',
'.flv'
]);
const enhanceLive = {
name: 'enhance-live',
description: '增强直播(原画画质、其他修复)',
onLive ({ addStyle, onBeforeFetch, onResponse }) {
let forceHighestQuality = true;
const urlMap = flru(300);
// 还得帮叔叔修 bug,唉
addStyle(e$2`div[data-cy=EvaRenderer_LayerWrapper]:has(.player) { z-index: 999999; }`);
// 干掉些直播间没用的东西
addStyle(e$2`#welcome-area-bottom-vm, .web-player-icon-roomStatus { display: none !important; }`);
// 修复直播画质
onBeforeFetch((fetchArgs)=>{
if (!forceHighestQuality) return fetchArgs;
try {
const url = getUrlFromRequest(fetchArgs[0]);
if (url == null) return fetchArgs;
let finalUrl = url;
// if (mcdnRegexp.test(url) && disableMcdn) {
// return Promise.reject();
// }
if (qualityRegexp.test(url)) {
finalUrl = url.replace(qualityRegexp, '$1').replaceAll(hevcRegexp, '$1');
logger.info('force quality', url, '->', finalUrl);
urlMap.set(finalUrl, url);
}
if (smtcdnsRegexp.test(finalUrl)) {
finalUrl = finalUrl.replace(smtcdnsRegexp, '$1');
logger.info('drop smtcdns', url, '->', finalUrl);
}
fetchArgs[0] = finalUrl;
return fetchArgs;
} catch {
return fetchArgs;
}
});
const errorCounter = new ErrorCounter(30000);
onResponse((resp, fetchArgs, $fetch)=>{
if (liveCdnUrlKwFilter(resp.url) && !resp.ok) {
logger.error('force quality fail', resp.url, resp.status);
errorCounter.recordError();
if (forceHighestQuality && errorCounter.getErrorCount() >= 5) {
forceHighestQuality = false;
logger.error('Force quality failed! Falling back');
GM.notification('[Make Bilibili Great Then Ever Before] 已为您自动切换至播放器上选择的清晰度.', '最高清晰度可能不可用');
}
// If we have old url, we fetch old quality again
if (urlMap.has(resp.url)) {
const oldUrl = urlMap.get(resp.url);
logger.warn('');
return $fetch(oldUrl, fetchArgs[1]);
}
}
return resp;
});
}
};const fixCopyInCV = {
name: 'fix-copy-in-cv',
description: '修复文章复制功能',
onCV () {
if ('original' in unsafeWindow) defineReadonlyProperty(unsafeWindow.original, 'reprint', '1');
const holder = document.querySelector('.article-holder');
if (holder) {
holder.classList.remove('unable-reprint');
holder.addEventListener('copy', (e)=>e.stopImmediatePropagation(), true);
}
}
};const noAd = {
name: 'no-ad',
description: '防止叔叔通过广告给自己赚棺材钱',
any ({ addStyle }) {
// 去广告
/**
* 下面是叔叔家的垃圾前端在 computed 里写副作用检测 AdBlock 是否启用:
u = () => {
var A;
if (!h.value)
return !1;
const _ = "cm."
, v = "bilibili.com"
, f = ((A = h.value) == null ? void 0 : A.querySelectorAll(`a[href*="${_}${v}"]`)) || [];
for (let y = 0; y < f.length; y++)
if (window.getComputedStyle(f[y]).display == "none")
return !0;
return !1
我们只要 display 不是 none 就行了
}
*/ addStyle(e$2`
.adblock-tips,
.feed-card:has(.bili-video-card>div:empty),
a[href*="cm.bilibili.com"],
.desktop-download-tip,
.ad-report {
width: 1px !important;
height: 1px !important;
opacity: 0 !important;
pointer-events: none !important;
position: absolute !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border-width: 0 !important;
}
`);
if (unsafeWindow.__INITIAL_STATE__?.adData) for(const key in unsafeWindow.__INITIAL_STATE__.adData){
if (!Array.isArray(unsafeWindow.__INITIAL_STATE__.adData[key])) continue;
for (const item of unsafeWindow.__INITIAL_STATE__.adData[key]){
item.name = 'B 站未来有可能会倒闭,但绝不会变质';
item.pic = 'https://static.hdslb.com/images/transparent.gif';
item.url = 'https://space.bilibili.com/208259';
}
}
if (unsafeWindow.__INITIAL_STATE__?.elecFullInfo) unsafeWindow.__INITIAL_STATE__.elecFullInfo.list = [];
}
};function n(n){return (t,o)=>Math.floor(n()*(o-t+1))+t}const t=n(Math.random);function r(n){return function(e){return 1===e.length?e[0]:e[n(0,e.length-1)]}}const o=r(t);function e$1(n,t=true){let o,r=false;return t?(o=n(),()=>o):()=>(r||(r=true,o=n()),o)}const e=e$1;const PROXY_TF = 'proxy-tf-all-ws.bilivideo.com';
const FALLBACK_CDN_HOST = 'upos-sz-mirrorali.bilivideo.com';
const MCDN_UPGCXCODE_URL_HOSTNAME_TO_BE_REPLACED = 'make-bilibili-great-than-ever-before.secret-internal-do-not-use-or-you-will-be-fired.nxdomain.skk.moe';
const mirrorRegex = /^https?:\/\/(?:upos-\w+-(?!302)\w+|(?:upos|proxy)-tf-[^/]+)\.(?:bilivideo|akamaized)\.(?:com|net)\/upgcxcode/;
const mCdnTfRegex = /^https?:\/\/(?:(?:\d{1,3}\.){3}\d{1,3}|[^/]+\.mcdn\.bilivideo\.(?:com|cn|net))(?::\d{1,5})?\/v\d\/resource/;
const knownP2pCdnDomainPattern = o$1([
'302ppio',
'302kodo',
'.mcdn.bilivideo',
'szbdyd.com',
'.nexusedgeio.com',
'.ahdohpiechei.com',
'upos-sz-mirror14b.bilivideo.com' // mirror type, upgcxcode, but it has no valid SSL cert, its SSL cert is for PCDN (*.bilivideo.cn)
]);
function isP2PCDNDomain(hostname) {
if (knownP2pCdnDomainPattern(hostname)) return true;
// upos-sz-302ppio.bilivideo.com -> *.nexusedgeio.com
// upos-sz-302kodo.bilivideo.com -> *.ahdohpiechei.com
// pattern: *-*302*.*
const subdomain = hostname.split('.', 1)[0];
return subdomain.includes('302');
}
function createCDNUtil() {
// All upgcxcode hosts are interchangeable, so we collect them here
const mirror_type_upgcxcode_hosts = new Set();
const bcache_type_upgcxcode_hosts = new Set();
const cdnDatas = flru(200);
return {
saveAndParsePlayerInfo (json, meta) {
let dash;
if ('data' in json && typeof json.data === 'object' && json.data !== null && 'dash' in json.data && typeof json.data.dash === 'object' && json.data.dash !== null) // normal video player
dash = json.data.dash;
else if (// bangumi video player
'result' in json && typeof json.result === 'object' && json.result !== null && 'video_info' in json.result && typeof json.result.video_info === 'object' && json.result.video_info !== null && 'dash' in json.result.video_info && typeof json.result.video_info.dash === 'object' && json.result.video_info.dash !== null) dash = json.result.video_info.dash;
else {
logger.warn('Invalid Bilibili Playinfo data', {
json
});
return;
}
if ('video' in dash && Array.isArray(dash.video)) extractCDNFromVideoOrAudio(dash.video);
if ('audio' in dash && Array.isArray(dash.audio)) extractCDNFromVideoOrAudio(dash.audio);
logger.info('CDN URLs extracted', {
meta
});
return cdnDatas;
},
getReplacementCdnUrl (url, meta) {
let urlObj;
if (typeof url === 'string') {
if (url.startsWith('//')) urlObj = new URL('https:' + url);
else urlObj = new URL(url);
} else urlObj = url;
const key = urlObj.pathname + urlObj.search;
const data = cdnDatas.get(key);
if (data !== undefined) return data.getReplacementUrl(url);
logger.warn('No matching CDN URL Group found! Opt-in basic P2P replacement', {
meta,
url: urlObj.href,
key
});
return basicP2PReplacement(typeof url === 'string' ? new URL(url) : url, meta);
}
};
function extractCDNFromVideoOrAudio(data) {
// In the data there is an array of baseUrl/backupUrl objects
// Each array consists of different quality levels
// We do not care about the quality levels, just extract all URLs per group
// Which we will be matching against later
for (const videoOrAudio of data){
if (typeof videoOrAudio !== 'object' || videoOrAudio === null) continue;
const knownUrls = new Set();
if ('baseUrl' in videoOrAudio && typeof videoOrAudio.baseUrl === 'string') knownUrls.add(videoOrAudio.baseUrl);
if ('base_url' in videoOrAudio && typeof videoOrAudio.base_url === 'string') knownUrls.add(videoOrAudio.base_url);
if ('backupUrl' in videoOrAudio && Array.isArray(videoOrAudio.backupUrl)) videoOrAudio.backupUrl.forEach((url)=>knownUrls.add(url));
if ('backup_url' in videoOrAudio && Array.isArray(videoOrAudio.backup_url)) videoOrAudio.backup_url.forEach((url)=>knownUrls.add(url));
// After collecting all known URLs, we can now process them
const mirror_urls = new Set();
const bcache_urls = new Set();
const mcdn_tf_urls = new Set();
const mcdn_upgcxcode_urls = new Set();
const szbdyd_urls = new Set();
for (const urlStr of knownUrls)try {
if (urlStr.includes('/upgcxcode/')) {
if (mirrorRegex.test(urlStr)) {
const url = new URL(urlStr);
// Now we know this url is both upgcxcode type url and mirror type url
// Since all upgcxcode urls are interchangeable, we can collect its host
if (// It is possible for a mirror type url to also be a p2p cdn:
//
// upos-sz-mirrorcoso1.bilivideo.com os=mcdn
// upos-*-302.bilivideo.com (HTTP 302 p2p cdn)
url.searchParams.get('os') !== 'mcdn' && !isP2PCDNDomain(url.hostname)) {
mirror_type_upgcxcode_hosts.add(url.hostname);
// Now we know this url is mirror type url and not p2p cdn
// let's ensure it is HTTPS and add to mirror urls
url.protocol = 'https:';
url.port = '443';
mirror_urls.add(url.href);
} else {
// Now we know this url is mirror type url, upgcxcode url, and p2p cdn url
url.protocol = 'https:';
url.port = '443';
// since we will replace its hostname anyway, the original hostname
// does not matter, we use a fixed dummy hostname here, and better
// reduce duplicates in the Set<string>.
url.hostname = MCDN_UPGCXCODE_URL_HOSTNAME_TO_BE_REPLACED;
mcdn_upgcxcode_urls.add(url.href);
}
continue;
}
const url = new URL(urlStr);
// Now we know this is upgcxcode type url, but not mirror type url:
if (isP2PCDNDomain(url.hostname)) {
// *.mcdn.bilivideo.* (mcdn type url p2p cdn)
// upos-\w*-302.* (HTTP 302 p2p cdn)
url.protocol = 'https:';
url.port = '443';
// since we will replace its hostname anyway, the original hostname
// does not matter, we use a fixed dummy hostname here, and better
// reduce duplicates in the Set<string>.
url.hostname = MCDN_UPGCXCODE_URL_HOSTNAME_TO_BE_REPLACED;
mcdn_upgcxcode_urls.add(url.href);
} else {
// bcache type url (self hosted PoP):
// cn-sccd-cu-01-01.bilivideo.com
// (more details in https://rec.danmuji.org/dev/cdn-info/ )
// we can collect its host for later replacement
bcache_type_upgcxcode_hosts.add(url.hostname);
bcache_urls.add(urlStr);
}
continue;
}
if (mCdnTfRegex.test(urlStr)) {
// This is mcdn type url, a.k.a. pure IP cdn url or mcdn.bilivideo.*
mcdn_tf_urls.add(urlStr);
continue;
}
// szbdyd.com appears to be deprecated, but we still handle it just in case
if (urlStr.includes('szbdyd.com')) {
const url = new URL(urlStr);
url.protocol = 'https:';
// szbdyd hostname can be replaced with the value of xy_usource query param
// and if xy_usource is missing, we can replace to upgcxcode host
url.hostname = url.searchParams.get('xy_usource') ?? MCDN_UPGCXCODE_URL_HOSTNAME_TO_BE_REPLACED;
url.port = '443';
szbdyd_urls.add(url.href);
continue;
}
logger.error(`Unrecognized CDN URL pattern: ${urlStr}`);
} catch {
logger.debug('Failed to process CDN URL, skipping.', {
url: urlStr
});
}
let replacementType;
let getReplacementUrl;
switch(true){
// We always prefer mirror type urls when possible, so as long as we have some,
// we always pick one from them
case mirror_urls.size > 0:
{
logger.info('Found ' + mirror_urls.size + ' mirror type CDN URLs, future replacement will be chosen from these URLs.', {
mirror_urls
});
replacementType = 'mirror';
const mirrorUrlsArray = Array.from(mirror_urls);
if (mirrorUrlsArray.length === 1) {
getReplacementUrl = ()=>mirrorUrlsArray[0];
break;
}
getReplacementUrl = ()=>o(mirrorUrlsArray);
break;
}
// bcache urls are not as good as mirror urls, but still better than p2p cdn,
// we pick one from them when no mirror urls are available
case bcache_urls.size > 0:
{
logger.info('Found ' + bcache_urls.size + ' bcache type CDN URLs, future replacement will be chosen from these URLs.', {
bcache_urls
});
replacementType = 'bcache';
const bcacheUrlsArray = Array.from(bcache_urls);
if (bcacheUrlsArray.length === 1) {
getReplacementUrl = ()=>bcacheUrlsArray[0];
break;
}
getReplacementUrl = ()=>o(bcacheUrlsArray);
break;
}
// Next we try HTTP 302/MCDN upgcxcode urls, since we can replace their
// hosts w/ bcache/mirror type upgcxcode hosts, it is not that bad
case mcdn_upgcxcode_urls.size > 0:
{
logger.info('Found ' + mcdn_upgcxcode_urls.size + ' mcdn upgcxcode type CDN URLs, future replacement will be chosen from these URLs with host replaced.', {
mcdn_upgcxcode_urls
});
replacementType = 'mcdn upgcxcode -> host replacement';
const mcdnUpgcxcodeUrlsArray = Array.from(mcdn_upgcxcode_urls);
if (mcdnUpgcxcodeUrlsArray.length === 1) {
getReplacementUrl = ()=>replaceUpgcxcodeHost(mcdnUpgcxcodeUrlsArray[0]);
break;
}
getReplacementUrl = ()=>replaceUpgcxcodeHost(o(mcdnUpgcxcodeUrlsArray));
break;
}
// Next we try szbdyd.com urls with either xy_usource or upgcxcode host replacement
case szbdyd_urls.size > 0:
{
logger.info('Found ' + szbdyd_urls.size + ' szbdyd.com type CDN URLs, future replacement will be chosen from these URLs with xy_usource or upgcxcode host replacement.', {
szbdyd_urls
});
replacementType = 'szbdyd.com -> xy_usource or upgcxcode host replacement';
const xyusourceUrlsArray = Array.from(szbdyd_urls);
getReplacementUrl = ()=>{
const picked = o(xyusourceUrlsArray);
const url = new URL(picked);
// If the URL does not have xy_usource, we need to replace with upgcxcode host
if (url.hostname === MCDN_UPGCXCODE_URL_HOSTNAME_TO_BE_REPLACED) // need to replace with upgcxcode host
return replaceUpgcxcodeHost(url);
return url.href;
};
break;
}
// We are left with pure IP cdn urls, or mcdn.bilivideo.* urls that are not
// upgcxcode type, we can return proxy-wrapped mcdn tf url
case mcdn_tf_urls.size > 0:
{
logger.info('Found ' + mcdn_tf_urls.size + ' mcdn tf type CDN URLs, future replacement will be proxy-wrapped.', {
mcdn_tf_urls
});
replacementType = 'mcdn tf -> proxy-wrapped';
const mcdnTfUrlsArray = Array.from(mcdn_tf_urls);
getReplacementUrl = ()=>{
const proxyUrl = new URL(`https://${PROXY_TF}`);
proxyUrl.searchParams.set('url', o(mcdnTfUrlsArray));
return proxyUrl.href;
};
break;
}
default:
logger.error('Failed to get replacement CDN URL', {
knownUrls
});
replacementType = 'none';
getReplacementUrl = (url)=>basicP2PReplacement(typeof url === 'string' ? new URL(url) : url, 'getReplacementCdnUrl fallback');
break;
}
knownUrls.forEach((url)=>{
const urlObj = new URL(url);
const key = urlObj.pathname + urlObj.search;
cdnDatas.set(key, {
replacementType,
getReplacementUrl,
// Optional meta
mirror_urls,
bcache_urls,
mcdn_upgcxcode_urls,
szbdyd_urls,
mcdn_tf_urls
});
});
}
}
function replaceUpgcxcodeHost(url) {
const urlObj = typeof url === 'string' ? new URL(url) : url;
urlObj.protocol = 'https:';
urlObj.port = '443';
if (mirror_type_upgcxcode_hosts.size > 0) {
const mirror_type_upgcxcode_hosts_array = Array.from(mirror_type_upgcxcode_hosts);
urlObj.hostname = o(mirror_type_upgcxcode_hosts_array);
return urlObj.href;
}
if (bcache_type_upgcxcode_hosts.size > 0) {
const bcache_type_upgcxcode_hosts_array = Array.from(bcache_type_upgcxcode_hosts);
urlObj.hostname = o(bcache_type_upgcxcode_hosts_array);
return urlObj.href;
}
urlObj.hostname = FALLBACK_CDN_HOST;
return urlObj.href;
}
function basicP2PReplacement(url, meta) {
const urlStr = url.href;
if (urlStr.includes('/upgcxcode/')) {
// Even if we have not collected any CDN info yet, we can still try our best to avoid P2P CDNs
if (mirrorRegex.test(urlStr)) {
// Now we know this url is both upgcxcode type url and mirror type url
// Since all upgcxcode urls are interchangeable, we can collect its host
if (// It is possible for a mirror type url to also be a p2p cdn:
//
// upos-sz-mirrorcoso1.bilivideo.com os=mcdn
// upos-\w*-302.* (HTTP 302 p2p cdn)
url.searchParams.get('os') !== 'mcdn' && !isP2PCDNDomain(url.hostname)) {
mirror_type_upgcxcode_hosts.add(url.hostname);
// Now we know this url is mirror type url and not p2p cdn
// let's ensure it is HTTPS and add to mirror urls
url.protocol = 'https:';
url.port = '443';
return url.href;
}
// Now we know this url is os=mcdn/http 302 url, let's replace its host
return replaceUpgcxcodeHost(url);
}
// Now we know this is upgcxcode type url, but not mirror type url:
if (isP2PCDNDomain(url.hostname)) // *.mcdn.bilivideo.* (mcdn type url p2p cdn)
// upos-\w*-302.* (HTTP 302 p2p cdn)
return replaceUpgcxcodeHost(url);
// bcache type url (self hosted PoP):
// cn-sccd-cu-01-01.bilivideo.com
// (more details in https://rec.danmuji.org/dev/cdn-info/ )
// we can collect its host for later replacement
bcache_type_upgcxcode_hosts.add(url.hostname);
return urlStr;
}
// szbdyd.com appears to be deprecated, but we still handle it just in case
if (urlStr.includes('szbdyd.com')) {
const xy_usource = url.searchParams.get('xy_usource');
if (xy_usource) {
url.protocol = 'https:';
url.port = '443';
url.hostname = xy_usource;
return url.href;
}
return replaceUpgcxcodeHost(url);
}
if (mCdnTfRegex.test(urlStr)) {
const proxyUrl = new URL(`https://${PROXY_TF}`);
proxyUrl.searchParams.set('url', urlStr);
return proxyUrl.href;
}
logger.error('Basic P2P replacement failed!', {
meta,
url: urlStr
});
return urlStr;
}
}
const getCDNUtil = e(createCDNUtil);function x(n,r="value"){throw TypeError(`Unexpected ${r}: ${n} as ${JSON.stringify(typeof n)}, should be "never"`)}function onDOMContentLoaded(callback) {
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', callback, {
once: true
});
else callback();
}
function onLoaded(callback) {
if (document.readyState === 'complete') callback();
else // eslint-disable-next-line no-restricted-globals -- use sandboxed event handler
window.addEventListener('load', callback, {
once: true
});
}const knownNonVideoPattern = o$1([
'bilibili.com',
'hdslb.com',
'bvc.bilivideo.com',
'bvc-drm.bilivideo.com'
]);
function isKnownNonVideoUrl(url) {
const urlStr = url.toString();
if (knownNonVideoPattern(urlStr)) return true;
if (typeof url === 'string') return url.startsWith('data:') || url.startsWith('blob:');
return url.protocol === 'data:' || url.protocol === 'blob:';
}
function isObject(value) {
return typeof value === 'object' && value !== null;
}
const noP2P = {
name: 'no-p2p',
description: '防止叔叔用 P2P CDN 省下纸钱',
any ({ onXhrOpen, onBeforeFetch, onXhrResponse }) {
class MockPCDNLoader {
}
class MockBPP2PSDK {
on = o$2;
}
class MockSeederSDK {
}
defineReadonlyProperty(unsafeWindow, 'PCDNLoader', MockPCDNLoader);
defineReadonlyProperty(unsafeWindow, 'BPP2PSDK', MockBPP2PSDK);
defineReadonlyProperty(unsafeWindow, 'SeederSDK', MockSeederSDK);
if (isObject(unsafeWindow.__playinfo__)) getCDNUtil().saveAndParsePlayerInfo(unsafeWindow.__playinfo__, 'unsafeWindow.__playinfo__');
else {
logger.warn('No unsafeWindow.__playinfo__ found, waiting for a microtask and check again.', {
json: unsafeWindow.__playinfo__
});
Promise.resolve().finally(()=>{
if (isObject(unsafeWindow.__playinfo__)) getCDNUtil().saveAndParsePlayerInfo(unsafeWindow.__playinfo__, 'unsafeWindow.__playinfo__ (microtask)');
else {
logger.warn('No unsafeWindow.__playinfo__ found in microtask either, waiting for DOMContentLoaded and check again.', {
json: unsafeWindow.__playinfo__
});
onDOMContentLoaded(()=>{
if (isObject(unsafeWindow.__playinfo__)) getCDNUtil().saveAndParsePlayerInfo(unsafeWindow.__playinfo__, 'unsafeWindow.__playinfo__ (DOMContentLoaded)');
});
}
});
}
onXhrResponse((_method, url, response, _xhr)=>{
if (url.toString().includes('api.bilibili.com/x/player/wbi/playurl') && typeof response === 'string') try {
getCDNUtil().saveAndParsePlayerInfo(JSON.parse(response), 'playurl XHR API');
} catch (e) {
logger.error('Failed to parse playinfo XHR API JSON', e, {
response
});
}
return response;
});
// Patch new Native Player
(function(HTMLMediaElementPrototypeSrcDescriptor) {
Object.defineProperty(unsafeWindow.HTMLMediaElement.prototype, 'src', {
...HTMLMediaElementPrototypeSrcDescriptor,
set (value) {
if (typeof value !== 'string') value = String(value);
if (!value.startsWith('blob:') && !value.startsWith('data:')) // we don't care about blob urls
// they will use another way to fetch the real url and turn it into blob url anyway
// we can intercept that fetch/XHR instead
try {
value = getCDNUtil().getReplacementCdnUrl(value, 'HTMLMediaElement.prototype.src');
} catch (e) {
logger.error('Failed to handle HTMLMediaElement.prototype.src setter', e, {
value
});
}
HTMLMediaElementPrototypeSrcDescriptor?.set?.call(this, value);
}
});
})(Object.getOwnPropertyDescriptor(unsafeWindow.HTMLMediaElement.prototype, 'src'));
onXhrOpen((xhrOpenArgs)=>{
const xhrUrl = xhrOpenArgs[1];
if (isKnownNonVideoUrl(xhrUrl)) return xhrOpenArgs;
try {
xhrOpenArgs[1] = getCDNUtil().getReplacementCdnUrl(xhrUrl, 'XMLHttpRequest.prototype.open');
} catch (e) {
logger.error('Failed to replace P2P for XMLHttpRequest.prototype.open', e, {
xhrUrl
});
}
return xhrOpenArgs;
});
onBeforeFetch((fetchArgs)=>{
let input = fetchArgs[0];
if (typeof input === 'string' || 'href' in input) {
if (!isKnownNonVideoUrl(input)) {
input = getCDNUtil().getReplacementCdnUrl(input, 'fetch');
fetchArgs[0] = input;
}
} else if ('url' in input) {
if (!isKnownNonVideoUrl(input.url)) {
input = new Request(getCDNUtil().getReplacementCdnUrl(input.url, 'fetch'), input);
fetchArgs[0] = input;
}
} else x(input, 'fetchArgs[0]');
return fetchArgs;
});
}
};const neverResolvedPromise = new Promise(o$2);
const noopNeverResolvedPromise = ()=>neverResolvedPromise;
// based on uBlock Origin's no-webrtc
// https://github.com/gorhill/uBlock/blob/6c228a8bfdcfc14140cdd3967270df28598c1aaf/src/js/resources/scriptlets.js#L2216
const noWebRTC = {
name: 'no-webrtc',
description: '通过禁用 WebRTC 防止叔叔省下棺材钱',
any () {
const rtcPcNames = [];
if ('RTCPeerConnection' in unsafeWindow) rtcPcNames.push('RTCPeerConnection');
if ('webkitRTCPeerConnection' in unsafeWindow) rtcPcNames.push('webkitRTCPeerConnection');
if ('mozRTCPeerConnection' in unsafeWindow) rtcPcNames.push('mozRTCPeerConnection');
const rtcDcNames = [];
if ('RTCDataChannel' in unsafeWindow) rtcDcNames.push('RTCDataChannel');
if ('webkitRTCDataChannel' in unsafeWindow) rtcDcNames.push('webkitRTCDataChannel');
if ('mozRTCDataChannel' in unsafeWindow) rtcDcNames.push('mozRTCDataChannel');
class MockDataChannel {
static{
this.prototype.close = o$2;
this.prototype.send = o$2;
this.prototype.addEventListener = o$2;
this.prototype.removeEventListener = o$2;
this.prototype.onbufferedamountlow = o$2;
// eslint-disable-next-line sukka/unicorn/prefer-add-event-listener -- mock
this.prototype.onclose = o$2;
// eslint-disable-next-line sukka/unicorn/prefer-add-event-listener -- mock
this.prototype.onerror = o$2;
// eslint-disable-next-line sukka/unicorn/prefer-add-event-listener -- mock
this.prototype.onmessage = o$2;
}
toString() {
return '[object RTCDataChannel]';
}
}
class MockRTCSessionDescription {
type;
sdp;
constructor(init){
this.type = init.type;
this.sdp = init.sdp || '';
}
toJSON() {
return {
type: this.type,
sdp: this.sdp
};
}
toString() {
return '[object RTCSessionDescription]';
}
}
const mockedRtcSessionDescription = new MockRTCSessionDescription({
type: 'offer',
sdp: ''
});
class MockRTCPeerConnection {
createDataChannel() {
return new MockDataChannel();
}
static{
this.prototype.close = o$2;
this.prototype.createOffer = noopNeverResolvedPromise;
this.prototype.setLocalDescription = o$2;
this.prototype.setRemoteDescription = o$2;
this.prototype.addEventListener = o$2;
this.prototype.removeEventListener = o$2;
this.prototype.addIceCandidate = o$2;
this.prototype.setConfiguration = o$2;
this.prototype.localDescription = mockedRtcSessionDescription;
this.prototype.createAnswer = noopNeverResolvedPromise;
this.prototype.onicecandidate = o$2;
}
toString() {
return '[object RTCPeerConnection]';
}
}
for (const rtc of rtcPcNames)defineReadonlyProperty(unsafeWindow, rtc, MockRTCPeerConnection);
for (const dc of rtcDcNames)defineReadonlyProperty(unsafeWindow, dc, MockDataChannel);
defineReadonlyProperty(unsafeWindow, 'RTCSessionDescription', MockRTCSessionDescription);
}
};const optimizeHomepage = {
name: 'optimize-homepage',
description: '首页广告去除和样式优化',
any ({ addStyle }) {
addStyle(e$2`
.feed2 .feed-card:has(a[href*="cm.bilibili.com"]),
.feed2 .feed-card:has(.bili-video-card:empty) {
width: 1px !important;
height: 1px !important;
opacity: 0 !important;
pointer-events: none !important;
position: absolute !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border-width: 0 !important;
}
.feed2 .container > * {
margin-top: 0 !important
}
`);
}
};const optimizeStory = {
name: 'optimize-story',
description: '动态页面优化',
onStory ({ addStyle }) {
addStyle(e$2`
html[wide] #app { display: flex; }
html[wide] .bili-dyn-home--member { box-sizing: border-box;padding: 0 10px;width: 100%;flex: 1; }
html[wide] .bili-dyn-content { width: initial; }
html[wide] main { margin: 0 8px;flex: 1;overflow: hidden;width: initial; }
#wide-mode-switch { margin-left: 0;margin-right: 20px; }
.bili-dyn-list__item:has(.bili-dyn-card-goods), .bili-dyn-list__item:has(.bili-rich-text-module.goods) { display: none !important }
`);
if (!localStorage.WIDE_OPT_OUT) document.documentElement.setAttribute('wide', 'wide');
onLoaded(()=>{
const tabContainer = document.querySelector('.bili-dyn-list-tabs__list');
const placeHolder = document.createElement('div');
placeHolder.style.flex = '1';
const switchButton = document.createElement('a');
switchButton.id = 'wide-mode-switch';
switchButton.className = 'bili-dyn-list-tabs__item';
switchButton.textContent = '宽屏模式';
switchButton.addEventListener('click', (e)=>{
e.preventDefault();
if (localStorage.WIDE_OPT_OUT) {
localStorage.removeItem('WIDE_OPT_OUT');
document.documentElement.setAttribute('wide', 'wide');
} else {
localStorage.setItem('WIDE_OPT_OUT', '1');
document.documentElement.removeAttribute('wide');
}
});
tabContainer?.appendChild(placeHolder);
tabContainer?.appendChild(switchButton);
});
}
};function toggleMode(enabled) {
if (enabled) document.body.setAttribute('video-fit', '');
else document.body.removeAttribute('video-fit');
}
const playerVideoFit = {
name: 'player-video-fit',
description: '播放器视频裁切模式',
onVideo ({ addStyle }) {
addStyle(e$2`body[video-fit] #bilibili-player video { object-fit: cover; } .bpx-player-ctrl-setting-fit-mode { display: flex;width: 100%;height: 32px;line-height: 32px; } .bpx-player-ctrl-setting-box .bui-panel-wrap, .bpx-player-ctrl-setting-box .bui-panel-item { min-height: 172px !important; }`);
let timer;
function injectButton() {
if (!document.querySelector('.bpx-player-ctrl-setting-menu-left')) return;
self.clearInterval(timer);
const parent = document.querySelector('.bpx-player-ctrl-setting-menu-left');
const item = document.createElement('div');
item.className = 'bpx-player-ctrl-setting-fit-mode bui bui-switch';
item.innerHTML = '<input class="bui-switch-input" type="checkbox"><label class="bui-switch-label"><span class="bui-switch-name">裁切模式</span><span class="bui-switch-body"><span class="bui-switch-dot"><span></span></span></span></label>';
parent?.insertBefore(item, document.querySelector('.bpx-player-ctrl-setting-more'));
document.querySelector('.bpx-player-ctrl-setting-fit-mode input')?.addEventListener('change', (e)=>toggleMode(e.target.checked));
const panelItem = document.querySelector('.bpx-player-ctrl-setting-box .bui-panel-item');
if (panelItem) panelItem.style.height = '';
}
timer = self.setInterval(injectButton, 200);
}
};const removeBlackBackdropFilter = {
name: 'remove-black-backdrop-filter',
description: '去除叔叔去世时的全站黑白效果',
any ({ addStyle }) {
addStyle(e$2`html, body { -webkit-filter: none !important; filter: none !important; }`);
}
};const uselessUrlParams = [
'buvid',
'is_story_h5',
'launch_id',
'live_from',
'mid',
'session_id',
'timestamp',
'up_id',
'vd_source',
'trackid',
/^share/,
/^spm/
];
const removeUselessUrlParams = {
name: 'remove-useless-url-params',
description: '清理 URL 中的无用参数',
any () {
unsafeWindow.history.replaceState(undefined, '', removeTracking(location.href));
// eslint-disable-next-line @typescript-eslint/unbound-method -- called with Reflect.apply
const pushState = unsafeWindow.history.pushState;
unsafeWindow.history.pushState = function(state, unused, url) {
return Reflect.apply(pushState, this, [
state,
unused,
removeTracking(url)
]);
};
// eslint-disable-next-line @typescript-eslint/unbound-method -- called with Reflect.apply
const replaceState = unsafeWindow.history.replaceState;
unsafeWindow.history.replaceState = function(state, unused, url) {
return Reflect.apply(replaceState, this, [
state,
unused,
removeTracking(url)
]);
};
}
};
function removeTracking(url) {
if (!url) return url;
try {
if (typeof url === 'string') url = new URL(url, unsafeWindow.location.href);
if (!url.search) return url;
const keys = Array.from(url.searchParams.keys());
for (const key of keys)for (const item of uselessUrlParams){
if (typeof item === 'string') {
if (item === key) url.searchParams.delete(key);
} else if ('test' in item && item.test(key)) url.searchParams.delete(key);
}
return url.href;
} catch (e) {
logger.error('Failed to remove useless urlParams', e);
return url;
}
}// 去除鸿蒙字体,强制使用系统默认字体
const useSystemFonts = {
name: 'use-system-fonts',
description: '去除鸿蒙字体,强制使用系统默认字体',
any ({ addStyle }) {
document.querySelectorAll('link[href*="/jinkela/long/font/"]').forEach((x)=>x.remove());
addStyle(e$2`html, body { font-family: system-ui !important; }`);
}
};// only call once, since fucking Bilibili now storming us with AV1 check
const logAv1Disabled = {
MediaSource_isTypeSupported () {
logger.info('AV1 disabled!', {
meta: 'MediaSource.isTypeSupported'
});
},
HTMLVideoElement_canPlayType () {
logger.info('AV1 disabled!', {
meta: 'HTMLVideoElement.prototype.canPlayType'
});
}
};
const disableAV1 = {
name: 'disable-av1',
description: '防止叔叔用 AV1 格式燃烧你的 CPU 并省下棺材钱',
any ({ onlyCallOnce }) {
((origCanPlayType)=>{
// eslint-disable-next-line sukka/class-prototype -- override native method
HTMLMediaElement.prototype.canPlayType = function(type) {
if (type.includes('av01')) {
onlyCallOnce(logAv1Disabled.HTMLVideoElement_canPlayType);
return '';
}
return origCanPlayType.call(this, type);
};
// eslint-disable-next-line @typescript-eslint/unbound-method -- override native method
})(HTMLMediaElement.prototype.canPlayType);
((origIsTypeSupported)=>{
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- can be nullable
if (origIsTypeSupported == null) return false;
unsafeWindow.MediaSource.isTypeSupported = function(type) {
if (type.includes('av01')) {
onlyCallOnce(logAv1Disabled.MediaSource_isTypeSupported);
return false;
}
return origIsTypeSupported.call(this, type);
};
// eslint-disable-next-line @typescript-eslint/unbound-method -- override native method
})(unsafeWindow.MediaSource.isTypeSupported);
}
};const messages = {
AbortError: "A request was aborted, for example through a call to IDBTransaction.abort.",
ConstraintError: "A mutation operation in the transaction failed because a constraint was not satisfied. For example, an object such as an object store or index already exists and a request attempted to create a new one.",
DataError: "Data provided to an operation does not meet requirements.",
InvalidAccessError: "An invalid operation was performed on an object. For example transaction creation attempt was made, but an empty scope was provided.",
InvalidStateError: "An operation was called on an object on which it is not allowed or at a time when it is not allowed. Also occurs if a request is made on a source object that has been deleted or removed. Use TransactionInactiveError or ReadOnlyError when possible, as they are more specific variations of InvalidStateError.",
NotFoundError: "The operation failed because the requested database object could not be found. For example, an object store did not exist but was being opened.",
ReadOnlyError: 'The mutating operation was attempted in a "readonly" transaction.',
TransactionInactiveError: "A request was placed against a transaction which is currently not active, or which is finished.",
VersionError: "An attempt was made to open a database using a lower version than the existing version."
};
// Cannot set an error code on an error using the normal setter;
// this leads to "Cannot set property code of which has only a getter"
const setErrorCode = (error, value) => {
Object.defineProperty(error, 'code', {
value,
writable: false,
enumerable: true,
configurable: false
});
};
class AbortError extends DOMException {
constructor(message = messages.AbortError) {
super(message, "AbortError");
}
}
class ConstraintError extends DOMException {
constructor(message = messages.ConstraintError) {
super(message, "ConstraintError");
}
}
class DataError extends DOMException {
constructor(message = messages.DataError) {
super(message, "DataError");
setErrorCode(this, 0);
}
}
class InvalidAccessError extends DOMException {
constructor(message = messages.InvalidAccessError) {
super(message, "InvalidAccessError");
}
}
class InvalidStateError extends DOMException {
constructor(message = messages.InvalidStateError) {
super(message, "InvalidStateError");
setErrorCode(this, 11);
}
}
class NotFoundError extends DOMException {
constructor(message = messages.NotFoundError) {
super(message, "NotFoundError");
}
}
class ReadOnlyError extends DOMException {
constructor(message = messages.ReadOnlyError) {
super(message, "ReadOnlyError");
}
}
class SyntaxError extends DOMException {
constructor(message = messages.VersionError) {
super(message, "SyntaxError");
setErrorCode(this, 12);
}
}
class TransactionInactiveError extends DOMException {
constructor(message = messages.TransactionInactiveError) {
super(message, "TransactionInactiveError");
setErrorCode(this, 0);
}
}
class VersionError extends DOMException {
constructor(message = messages.VersionError) {
super(message, "VersionError");
}
}function isSharedArrayBuffer(input) {
return typeof SharedArrayBuffer !== "undefined" && input instanceof SharedArrayBuffer;
}const INVALID_TYPE = Symbol("INVALID_TYPE");
const INVALID_VALUE = Symbol("INVALID_VALUE");
// https://w3c.github.io/IndexedDB/#convert-value-to-key
// The "without exceptions" version is because we typically want to throw exceptions (DataError) but not for
// the "is potentially valid key range" routine.
const valueToKeyWithoutThrowing = (input, seen) => {
if (typeof input === "number") {
if (isNaN(input)) {
// If input is NaN then return "invalid value".
return INVALID_VALUE;
}
return input;
} else if (Object.prototype.toString.call(input) === "[object Date]") {
const ms = input.valueOf();
if (isNaN(ms)) {
// If ms is NaN then return "invalid value".
return INVALID_VALUE;
}
return new Date(ms);
} else if (typeof input === "string") {
return input;
} else if (
// https://w3c.github.io/IndexedDB/#ref-for-dfn-buffer-source-type
input instanceof ArrayBuffer || isSharedArrayBuffer(input) || typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView && ArrayBuffer.isView(input)) {
// We can't consistently test detachedness, so instead we check if byteLength === 0
// This isn't foolproof, but there's no perfect way to detect if Uint8Arrays or
// SharedArrayBuffers are detached
if ("detached" in input ? input.detached : input.byteLength === 0) {
// If input is detached then return "invalid value".
return INVALID_VALUE;
}
let arrayBuffer;
let offset = 0;
let length = 0;
if (input instanceof ArrayBuffer || isSharedArrayBuffer(input)) {
arrayBuffer = input;
length = input.byteLength;
} else {
arrayBuffer = input.buffer;
offset = input.byteOffset;
length = input.byteLength;
}
return arrayBuffer.slice(offset, offset + length);
} else if (Array.isArray(input)) {
if (seen === undefined) {
seen = new Set();
} else if (seen.has(input)) {
// If seen contains input, then return "invalid value".
return INVALID_VALUE;
}
seen.add(input);
// This algorithm is tricky to account for `bindings-inject-keys-bypass.any.js`. We _should_ return early when
// encountering an invalid key/type, but we also need to avoid triggering `Object.prototype['10']` if it's been
// overridden. One simple way to do this (and which doesn't rely on sparse arrays or other exotic solutions that
// could cause de-opts) is to use `Array.from()` with a mapper function, which does not trigger the prototype
// setter [1]. It does prevent an early return, but we can at least short-circuit inside the mapper function
// (which isn't strictly necessary to pass the WPTs, but is closer to the spec).
// [1]: See https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.from, specifically
// the chain CreateDataPropertyOrThrow -> CreateDataProperty -> DefineOwnProperty which defines
// the array element as an "own" property.
let hasInvalid = false;
const keys = Array.from({
length: input.length
}, (_, i) => {
if (hasInvalid) {
return;
}
const hop = Object.hasOwn(input, i);
if (!hop) {
// If hop is false, return "invalid value".
hasInvalid = true;
return;
}
const entry = input[i];
const key = valueToKeyWithoutThrowing(entry, seen);
// If key is "invalid value" or "invalid type" abort these steps and return "invalid value".
if (key === INVALID_VALUE || key === INVALID_TYPE) {
hasInvalid = true;
return;
}
return key;
});
if (hasInvalid) {
return INVALID_VALUE;
}
return keys;
} else {
// Otherwise: Return "invalid type".
return INVALID_TYPE;
}
};// https://w3c.github.io/IndexedDB/#convert-value-to-key
// Plus throwing a DataError for invalid value/invalid key, which is commonly done
// in lots of IndexedDB operations
const valueToKey = (input, seen) => {
const result = valueToKeyWithoutThrowing(input, seen);
if (result === INVALID_VALUE || result === INVALID_TYPE) {
// If key is "invalid value" or "invalid type", throw a "DataError" DOMException
throw new DataError();
}
return result;
};const getType = x => {
if (typeof x === "number") {
return "Number";
}
if (Object.prototype.toString.call(x) === "[object Date]") {
return "Date";
}
if (Array.isArray(x)) {
return "Array";
}
if (typeof x === "string") {
return "String";
}
if (x instanceof ArrayBuffer) {
return "Binary";
}
throw new DataError();
};
// https://w3c.github.io/IndexedDB/#compare-two-keys
const cmp = (first, second) => {
if (second === undefined) {
throw new TypeError();
}
first = valueToKey(first);
second = valueToKey(second);
const t1 = getType(first);
const t2 = getType(second);
if (t1 !== t2) {
if (t1 === "Array") {
return 1;
}
if (t1 === "Binary" && (t2 === "String" || t2 === "Date" || t2 === "Number")) {
return 1;
}
if (t1 === "String" && (t2 === "Date" || t2 === "Number")) {
return 1;
}
if (t1 === "Date" && t2 === "Number") {
return 1;
}
return -1;
}
if (t1 === "Binary") {
first = new Uint8Array(first);
second = new Uint8Array(second);
}
if (t1 === "Array" || t1 === "Binary") {
const length = Math.min(first.length, second.length);
for (let i = 0; i < length; i++) {
const result = cmp(first[i], second[i]);
if (result !== 0) {
return result;
}
}
if (first.length > second.length) {
return 1;
}
if (first.length < second.length) {
return -1;
}
return 0;
}
if (t1 === "Date") {
if (first.getTime() === second.getTime()) {
return 0;
}
} else {
if (first === second) {
return 0;
}
}
return first > second ? 1 : -1;
};// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#range-concept
class FDBKeyRange {
static only(value) {
if (arguments.length === 0) {
throw new TypeError();
}
value = valueToKey(value);
return new FDBKeyRange(value, value, false, false);
}
static lowerBound(lower, open = false) {
if (arguments.length === 0) {
throw new TypeError();
}
lower = valueToKey(lower);
return new FDBKeyRange(lower, undefined, open, true);
}
static upperBound(upper, open = false) {
if (arguments.length === 0) {
throw new TypeError();
}
upper = valueToKey(upper);
return new FDBKeyRange(undefined, upper, true, open);
}
static bound(lower, upper, lowerOpen = false, upperOpen = false) {
if (arguments.length < 2) {
throw new TypeError();
}
const cmpResult = cmp(lower, upper);
if (cmpResult === 1 || cmpResult === 0 && (lowerOpen || upperOpen)) {
throw new DataError();
}
lower = valueToKey(lower);
upper = valueToKey(upper);
return new FDBKeyRange(lower, upper, lowerOpen, upperOpen);
}
constructor(lower, upper, lowerOpen, upperOpen) {
this.lower = lower;
this.upper = upper;
this.lowerOpen = lowerOpen;
this.upperOpen = upperOpen;
}
// https://w3c.github.io/IndexedDB/#dom-idbkeyrange-includes
includes(key) {
if (arguments.length === 0) {
throw new TypeError();
}
key = valueToKey(key);
if (this.lower !== undefined) {
const cmpResult = cmp(this.lower, key);
if (cmpResult === 1 || cmpResult === 0 && this.lowerOpen) {
return false;
}
}
if (this.upper !== undefined) {
const cmpResult = cmp(this.upper, key);
if (cmpResult === -1 || cmpResult === 0 && this.upperOpen) {
return false;
}
}
return true;
}
get [Symbol.toStringTag]() {
return "IDBKeyRange";
}
}// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-extracting-a-key-from-a-value-using-a-key-path
const extractKey = (keyPath, value) => {
if (Array.isArray(keyPath)) {
const result = [];
for (let item of keyPath) {
// This doesn't make sense to me based on the spec, but it is needed to pass the W3C KeyPath tests (see same
// comment in validateKeyPath)
if (item !== undefined && item !== null && typeof item !== "string" && item.toString) {
item = item.toString();
}
const key = extractKey(item, value).key;
result.push(valueToKey(key));
}
return {
type: "found",
key: result
};
}
if (keyPath === "") {
return {
type: "found",
key: value
};
}
let remainingKeyPath = keyPath;
let object = value;
while (remainingKeyPath !== null) {
let identifier;
const i = remainingKeyPath.indexOf(".");
if (i >= 0) {
identifier = remainingKeyPath.slice(0, i);
remainingKeyPath = remainingKeyPath.slice(i + 1);
} else {
identifier = remainingKeyPath;
remainingKeyPath = null;
}
// special cases: https://w3c.github.io/IndexedDB/#evaluate-a-key-path-on-a-value
const isSpecialIdentifier = identifier === "length" && (typeof object === "string" || Array.isArray(object)) || (identifier === "size" || identifier === "type") && typeof Blob !== "undefined" && object instanceof Blob || (identifier === "name" || identifier === "lastModified") && typeof File !== "undefined" && object instanceof File;
if (!isSpecialIdentifier && (typeof object !== "object" || object === null || !Object.hasOwn(object, identifier))) {
return {
type: "notFound"
};
}
object = object[identifier];
}
return {
type: "found",
key: object
};
};// https://w3c.github.io/IndexedDB/#clone-value
// Note that we only need to call this during insertions because the spec does not expect any cloning during retrieval,
// only `StructuredDeserialize()` (e.g. see [1]). This is also only required for values, not keys, since keys do not
// require cloning during insertion (e.g. see [2]).
// [1]: https://w3c.github.io/IndexedDB/#retrieve-multiple-items-from-an-object-store
// [2]: https://w3c.github.io/IndexedDB/#add-or-put
function cloneValueForInsertion(value, transaction) {
// Assert: transaction’s state is active.
if (transaction._state !== "active") {
throw new Error("Assert: transaction state is active");
}
// Set transaction’s state to inactive.
transaction._state = "inactive";
try {
// Let serialized be StructuredSerializeForStorage(value).
// Let clone be ? StructuredDeserialize(serialized, targetRealm).
// Return clone.
return structuredClone(value);
} finally {
// Set transaction’s state to active.
transaction._state = "active";
}
}const getEffectiveObjectStore = cursor => {
if (cursor.source instanceof FDBObjectStore) {
return cursor.source;
}
return cursor.source.objectStore;
};
// This takes a key range, a list of lower bounds, and a list of upper bounds and combines them all into a single key
// range. It does not handle gt/gte distinctions, because it doesn't really matter much anyway, since for next/prev
// cursor iteration it'd also have to look at values to be precise, which would be complicated. This should get us 99%
// of the way there.
const makeKeyRange = (range, lowers, uppers) => {
// Start with bounds from range
let lower = range !== undefined ? range.lower : undefined;
let upper = range !== undefined ? range.upper : undefined;
// Augment with values from lowers and uppers
for (const lowerTemp of lowers) {
if (lowerTemp === undefined) {
continue;
}
if (lower === undefined || cmp(lower, lowerTemp) === 1) {
lower = lowerTemp;
}
}
for (const upperTemp of uppers) {
if (upperTemp === undefined) {
continue;
}
if (upper === undefined || cmp(upper, upperTemp) === -1) {
upper = upperTemp;
}
}
if (lower !== undefined && upper !== undefined) {
return FDBKeyRange.bound(lower, upper);
}
if (lower !== undefined) {
return FDBKeyRange.lowerBound(lower);
}
if (upper !== undefined) {
return FDBKeyRange.upperBound(upper);
}
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor
class FDBCursor {
_gotValue = false;
_position = undefined; // Key of previously returned record
_objectStorePosition = undefined;
_keyOnly = false;
_key = undefined;
_primaryKey = undefined;
constructor(source, range, direction = "next", request, keyOnly = false) {
this._range = range;
this._source = source;
this._direction = direction;
this._request = request;
this._keyOnly = keyOnly;
}
// Read only properties
get source() {
return this._source;
}
set source(val) {
/* For babel */
}
get request() {
return this._request;
}
set request(val) {
/* For babel */
}
get direction() {
return this._direction;
}
set direction(val) {
/* For babel */
}
get key() {
return this._key;
}
set key(val) {
/* For babel */
}
get primaryKey() {
return this._primaryKey;
}
set primaryKey(val) {
/* For babel */
}
// https://w3c.github.io/IndexedDB/#iterate-a-cursor
_iterate(key, primaryKey) {
const sourceIsObjectStore = this.source instanceof FDBObjectStore;
// Can't use sourceIsObjectStore because TypeScript
const records = this.source instanceof FDBObjectStore ? this.source._rawObjectStore.records : this.source._rawIndex.records;
let foundRecord;
if (this.direction === "next") {
const range = makeKeyRange(this._range, [key, this._position], []);
for (const record of records.values(range)) {
const cmpResultKey = key !== undefined ? cmp(record.key, key) : undefined;
const cmpResultPosition = this._position !== undefined ? cmp(record.key, this._position) : undefined;
if (key !== undefined) {
if (cmpResultKey === -1) {
continue;
}
}
if (primaryKey !== undefined) {
if (cmpResultKey === -1) {
continue;
}
const cmpResultPrimaryKey = cmp(record.value, primaryKey);
if (cmpResultKey === 0 && cmpResultPrimaryKey === -1) {
continue;
}
}
if (this._position !== undefined && sourceIsObjectStore) {
if (cmpResultPosition !== 1) {
continue;
}
}
if (this._position !== undefined && !sourceIsObjectStore) {
if (cmpResultPosition === -1) {
continue;
}
if (cmpResultPosition === 0 && cmp(record.value, this._objectStorePosition) !== 1) {
continue;
}
}
if (this._range !== undefined) {
if (!this._range.includes(record.key)) {
continue;
}
}
foundRecord = record;
break;
}
} else if (this.direction === "nextunique") {
// This could be done without iterating, if the range was defined slightly better (to handle gt/gte cases).
// But the performance difference should be small, and that wouldn't work anyway for directions where the
// value needs to be used (like next and prev).
const range = makeKeyRange(this._range, [key, this._position], []);
for (const record of records.values(range)) {
if (key !== undefined) {
if (cmp(record.key, key) === -1) {
continue;
}
}
if (this._position !== undefined) {
if (cmp(record.key, this._position) !== 1) {
continue;
}
}
if (this._range !== undefined) {
if (!this._range.includes(record.key)) {
continue;
}
}
foundRecord = record;
break;
}
} else if (this.direction === "prev") {
const range = makeKeyRange(this._range, [], [key, this._position]);
for (const record of records.values(range, "prev")) {
const cmpResultKey = key !== undefined ? cmp(record.key, key) : undefined;
const cmpResultPosition = this._position !== undefined ? cmp(record.key, this._position) : undefined;
if (key !== undefined) {
if (cmpResultKey === 1) {
continue;
}
}
if (primaryKey !== undefined) {
if (cmpResultKey === 1) {
continue;
}
const cmpResultPrimaryKey = cmp(record.value, primaryKey);
if (cmpResultKey === 0 && cmpResultPrimaryKey === 1) {
continue;
}
}
if (this._position !== undefined && sourceIsObjectStore) {
if (cmpResultPosition !== -1) {
continue;
}
}
if (this._position !== undefined && !sourceIsObjectStore) {
if (cmpResultPosition === 1) {
continue;
}
if (cmpResultPosition === 0 && cmp(record.value, this._objectStorePosition) !== -1) {
continue;
}
}
if (this._range !== undefined) {
if (!this._range.includes(record.key)) {
continue;
}
}
foundRecord = record;
break;
}
} else if (this.direction === "prevunique") {
let tempRecord;
const range = makeKeyRange(this._range, [], [key, this._position]);
for (const record of records.values(range, "prev")) {
if (key !== undefined) {
if (cmp(record.key, key) === 1) {
continue;
}
}
if (this._position !== undefined) {
if (cmp(record.key, this._position) !== -1) {
continue;
}
}
if (this._range !== undefined) {
if (!this._range.includes(record.key)) {
continue;
}
}
tempRecord = record;
break;
}
if (tempRecord) {
foundRecord = records.get(tempRecord.key);
}
}
let result;
if (!foundRecord) {
this._key = undefined;
if (!sourceIsObjectStore) {
this._objectStorePosition = undefined;
}
// "this instanceof FDBCursorWithValue" would be better and not require (this as any), but causes runtime
// error due to circular dependency.
if (!this._keyOnly && this.toString() === "[object IDBCursorWithValue]") {
this.value = undefined;
}
result = null;
} else {
this._position = foundRecord.key;
if (!sourceIsObjectStore) {
this._objectStorePosition = foundRecord.value;
}
this._key = foundRecord.key;
if (sourceIsObjectStore) {
this._primaryKey = structuredClone(foundRecord.key);
if (!this._keyOnly && this.toString() === "[object IDBCursorWithValue]") {
this.value = structuredClone(foundRecord.value);
}
} else {
this._primaryKey = structuredClone(foundRecord.value);
if (!this._keyOnly && this.toString() === "[object IDBCursorWithValue]") {
if (this.source instanceof FDBObjectStore) {
// Can't use sourceIsObjectStore because TypeScript
throw new Error("This should never happen");
}
const value = this.source.objectStore._rawObjectStore.getValue(foundRecord.value);
this.value = structuredClone(value);
}
}
this._gotValue = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias
result = this;
}
return result;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-update-IDBRequest-any-value
update(value) {
if (value === undefined) {
throw new TypeError();
}
const effectiveObjectStore = getEffectiveObjectStore(this);
const effectiveKey = Object.hasOwn(this.source, "_rawIndex") ? this.primaryKey : this._position;
const transaction = effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (transaction.mode === "readonly") {
throw new ReadOnlyError();
}
if (effectiveObjectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (!(this.source instanceof FDBObjectStore) && this.source._rawIndex.deleted) {
throw new InvalidStateError();
}
if (!this._gotValue || !Object.hasOwn(this, "value")) {
throw new InvalidStateError();
}
const clone = cloneValueForInsertion(value, transaction);
if (effectiveObjectStore.keyPath !== null) {
let tempKey;
try {
tempKey = extractKey(effectiveObjectStore.keyPath, clone).key;
} catch (err) {
/* Handled immediately below */
}
if (cmp(tempKey, effectiveKey) !== 0) {
throw new DataError();
}
}
const record = {
key: effectiveKey,
value: clone
};
return transaction._execRequestAsync({
operation: effectiveObjectStore._rawObjectStore.storeRecord.bind(effectiveObjectStore._rawObjectStore, record, false, transaction._rollbackLog),
source: this
});
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-advance-void-unsigned-long-count
advance(count) {
if (!Number.isInteger(count) || count <= 0) {
throw new TypeError();
}
const effectiveObjectStore = getEffectiveObjectStore(this);
const transaction = effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (effectiveObjectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (!(this.source instanceof FDBObjectStore) && this.source._rawIndex.deleted) {
throw new InvalidStateError();
}
if (!this._gotValue) {
throw new InvalidStateError();
}
if (this._request) {
this._request.readyState = "pending";
}
transaction._execRequestAsync({
operation: () => {
let result;
for (let i = 0; i < count; i++) {
result = this._iterate();
// Not sure why this is needed
if (!result) {
break;
}
}
return result;
},
request: this._request,
source: this.source
});
this._gotValue = false;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-continue-void-any-key
continue(key) {
const effectiveObjectStore = getEffectiveObjectStore(this);
const transaction = effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (effectiveObjectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (!(this.source instanceof FDBObjectStore) && this.source._rawIndex.deleted) {
throw new InvalidStateError();
}
if (!this._gotValue) {
throw new InvalidStateError();
}
if (key !== undefined) {
key = valueToKey(key);
const cmpResult = cmp(key, this._position);
if (cmpResult <= 0 && (this.direction === "next" || this.direction === "nextunique") || cmpResult >= 0 && (this.direction === "prev" || this.direction === "prevunique")) {
throw new DataError();
}
}
if (this._request) {
this._request.readyState = "pending";
}
transaction._execRequestAsync({
operation: this._iterate.bind(this, key),
request: this._request,
source: this.source
});
this._gotValue = false;
}
// hthttps://w3c.github.io/IndexedDB/#dom-idbcursor-continueprimarykey
continuePrimaryKey(key, primaryKey) {
const effectiveObjectStore = getEffectiveObjectStore(this);
const transaction = effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (effectiveObjectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (!(this.source instanceof FDBObjectStore) && this.source._rawIndex.deleted) {
throw new InvalidStateError();
}
if (this.source instanceof FDBObjectStore || this.direction !== "next" && this.direction !== "prev") {
throw new InvalidAccessError();
}
if (!this._gotValue) {
throw new InvalidStateError();
}
// Not sure about this
if (key === undefined || primaryKey === undefined) {
throw new DataError();
}
key = valueToKey(key);
const cmpResult = cmp(key, this._position);
if (cmpResult === -1 && this.direction === "next" || cmpResult === 1 && this.direction === "prev") {
throw new DataError();
}
const cmpResult2 = cmp(primaryKey, this._objectStorePosition);
if (cmpResult === 0) {
if (cmpResult2 <= 0 && this.direction === "next" || cmpResult2 >= 0 && this.direction === "prev") {
throw new DataError();
}
}
if (this._request) {
this._request.readyState = "pending";
}
transaction._execRequestAsync({
operation: this._iterate.bind(this, key, primaryKey),
request: this._request,
source: this.source
});
this._gotValue = false;
}
delete() {
const effectiveObjectStore = getEffectiveObjectStore(this);
const effectiveKey = Object.hasOwn(this.source, "_rawIndex") ? this.primaryKey : this._position;
const transaction = effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (transaction.mode === "readonly") {
throw new ReadOnlyError();
}
if (effectiveObjectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (!(this.source instanceof FDBObjectStore) && this.source._rawIndex.deleted) {
throw new InvalidStateError();
}
if (!this._gotValue || !Object.hasOwn(this, "value")) {
throw new InvalidStateError();
}
return transaction._execRequestAsync({
operation: effectiveObjectStore._rawObjectStore.deleteRecord.bind(effectiveObjectStore._rawObjectStore, effectiveKey, transaction._rollbackLog),
source: this
});
}
get [Symbol.toStringTag]() {
return "IDBCursor";
}
}class FDBCursorWithValue extends FDBCursor {
value = undefined;
constructor(source, range, direction, request) {
super(source, range, direction, request);
}
get [Symbol.toStringTag]() {
return "IDBCursorWithValue";
}
}const stopped = (event, listener) => {
return event.immediatePropagationStopped || event.eventPhase === event.CAPTURING_PHASE && listener.capture === false || event.eventPhase === event.BUBBLING_PHASE && listener.capture === true;
};
// http://www.w3.org/TR/dom/#concept-event-listener-invoke
const invokeEventListeners = (event, obj) => {
event.currentTarget = obj;
const errors = [];
const invoke = callbackOrObject => {
try {
const callback = typeof callbackOrObject === "function" ? callbackOrObject : callbackOrObject.handleEvent;
// @ts-expect-error EventCallback's types are not quite right here
callback.call(event.currentTarget, event);
} catch (err) {
errors.push(err);
}
};
// The callback might cause obj.listeners to mutate as we traverse it.
// Take a copy of the array so that nothing sneaks in and we don't lose
// our place.
for (const listener of obj.listeners.slice()) {
if (event.type !== listener.type || stopped(event, listener)) {
continue;
}
invoke(listener.callback);
}
const typeToProp = {
abort: "onabort",
blocked: "onblocked",
close: "onclose",
complete: "oncomplete",
error: "onerror",
success: "onsuccess",
upgradeneeded: "onupgradeneeded",
versionchange: "onversionchange"
};
const prop = typeToProp[event.type];
if (prop === undefined) {
throw new Error(`Unknown event type: "${event.type}"`);
}
const callback = event.currentTarget[prop];
if (callback) {
const listener = {
callback,
capture: false,
type: event.type
};
if (!stopped(event, listener)) {
invoke(listener.callback);
}
}
// we want to execute all listeners before deciding if we want to throw, because there could be an error thrown by
// the first listener, but the second should still be invoked
if (errors.length) {
throw new AggregateError(errors);
}
};
class FakeEventTarget {
listeners = [];
// These will be overridden in individual subclasses and made not readonly
addEventListener(type, callback, options) {
const capture = !!(typeof options === "object" && options ? options.capture : options);
this.listeners.push({
callback,
capture,
type
});
}
removeEventListener(type, callback, options) {
const capture = !!(typeof options === "object" && options ? options.capture : options);
const i = this.listeners.findIndex(listener => {
return listener.type === type && listener.callback === callback && listener.capture === capture;
});
this.listeners.splice(i, 1);
}
// http://www.w3.org/TR/dom/#dispatching-events
dispatchEvent(event) {
if (event.dispatched || !event.initialized) {
throw new InvalidStateError("The object is in an invalid state.");
}
event.isTrusted = false;
event.dispatched = true;
event.target = this;
// NOT SURE WHEN THIS SHOULD BE SET event.eventPath = [];
event.eventPhase = event.CAPTURING_PHASE;
for (const obj of event.eventPath) {
if (!event.propagationStopped) {
invokeEventListeners(event, obj);
}
}
event.eventPhase = event.AT_TARGET;
if (!event.propagationStopped) {
invokeEventListeners(event, event.target);
}
if (event.bubbles) {
event.eventPath.reverse();
event.eventPhase = event.BUBBLING_PHASE;
for (const obj of event.eventPath) {
if (!event.propagationStopped) {
invokeEventListeners(event, obj);
}
}
}
event.dispatched = false;
event.eventPhase = event.NONE;
event.currentTarget = null;
if (event.canceled) {
return false;
}
return true;
}
}class FDBRequest extends FakeEventTarget {
_result = null;
_error = null;
source = null;
transaction = null;
readyState = "pending";
onsuccess = null;
onerror = null;
get error() {
if (this.readyState === "pending") {
throw new InvalidStateError();
}
return this._error;
}
set error(value) {
this._error = value;
}
get result() {
if (this.readyState === "pending") {
throw new InvalidStateError();
}
return this._result;
}
set result(value) {
this._result = value;
}
get [Symbol.toStringTag]() {
return "IDBRequest";
}
}class FakeDOMStringList {
constructor(...values) {
this._values = values;
for (let i = 0; i < values.length; i++) {
this[i] = values[i];
}
}
contains(value) {
return this._values.includes(value);
}
item(i) {
if (i < 0 || i >= this._values.length) {
return null;
}
return this._values[i];
}
get length() {
return this._values.length;
}
[Symbol.iterator]() {
return this._values[Symbol.iterator]();
}
// Handled by proxy
// Used internally, should not be used by others. I could maybe get rid of these and replace rather than mutate, but too lazy to check the spec.
_push(...values) {
for (let i = 0; i < values.length; i++) {
this[this._values.length + i] = values[i];
}
this._values.push(...values);
}
_sort(...values) {
this._values.sort(...values);
for (let i = 0; i < this._values.length; i++) {
this[i] = this._values[i];
}
return this;
}
}// http://w3c.github.io/IndexedDB/#convert-a-value-to-a-key-range
const valueToKeyRange = (value, nullDisallowedFlag = false) => {
if (value instanceof FDBKeyRange) {
return value;
}
if (value === null || value === undefined) {
if (nullDisallowedFlag) {
throw new DataError();
}
return new FDBKeyRange(undefined, undefined, false, false);
}
const key = valueToKey(value);
return FDBKeyRange.only(key);
};// Keys provided as functions or arrays or objects need to be stringified
const convertKey = key => typeof key === 'object' && key ? key + '' : key;
// https://www.w3.org/TR/IndexedDB/#dom-idbobjectstore-keypath
function getKeyPath(keyPath) {
// It's important to clone the Array here because of the WPT test:
// "Different instances are returned from different store instances."
// Also note that the same instance must be returned across multiple gets
return Array.isArray(keyPath) ? keyPath.map(convertKey) : convertKey(keyPath);
}// https://www.w3.org/TR/IndexedDB/#is-a-potentially-valid-key-range
const isPotentiallyValidKeyRange = value => {
// If value is a key range, return true.
if (value instanceof FDBKeyRange) {
return true;
}
// Let key be the result of converting a value to a key with value.
const key = valueToKeyWithoutThrowing(value);
// If key is "invalid type" return false.
// Else return true.
return key !== INVALID_TYPE;
};// https://heycam.github.io/webidl/#EnforceRange
const enforceRange = (num, type) => {
const min = 0;
const max = type === "unsigned long" ? 4294967295 : 9007199254740991;
if (isNaN(num) || num < min || num > max) {
throw new TypeError();
}
if (num >= 0) {
return Math.floor(num);
}
};// https://www.w3.org/TR/IndexedDB/#create-request-to-retrieve-multiple-items
const extractGetAllOptions = (queryOrOptions, count, numArguments) => {
let query;
let direction;
if (queryOrOptions === undefined || queryOrOptions === null || isPotentiallyValidKeyRange(queryOrOptions)) {
// queryOrOptions is FDBKeyRange | Key | null | undefined
query = queryOrOptions;
if (numArguments > 1 && count !== undefined) {
count = enforceRange(count, "unsigned long");
}
} else {
// queryOrOptions is FDBGetAllOptions
const getAllOptions = queryOrOptions;
if (getAllOptions.query !== undefined) {
query = getAllOptions.query;
}
if (getAllOptions.count !== undefined) {
count = enforceRange(getAllOptions.count, "unsigned long");
}
if (getAllOptions.direction !== undefined) {
direction = getAllOptions.direction;
}
}
return {
query,
count,
direction
};
};const confirmActiveTransaction$1 = index => {
if (index._rawIndex.deleted || index.objectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (index.objectStore.transaction._state !== "active") {
throw new TransactionInactiveError();
}
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#idl-def-IDBIndex
class FDBIndex {
constructor(objectStore, rawIndex) {
this._rawIndex = rawIndex;
this._name = rawIndex.name;
this.objectStore = objectStore;
this.keyPath = getKeyPath(rawIndex.keyPath);
this.multiEntry = rawIndex.multiEntry;
this.unique = rawIndex.unique;
}
get name() {
return this._name;
}
// https://w3c.github.io/IndexedDB/#dom-idbindex-name
set name(name) {
const transaction = this.objectStore.transaction;
if (!transaction.db._runningVersionchangeTransaction) {
throw transaction._state === "active" ? new InvalidStateError() : new TransactionInactiveError();
}
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (this._rawIndex.deleted || this.objectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
name = String(name);
if (name === this._name) {
return;
}
if (this.objectStore.indexNames.contains(name)) {
throw new ConstraintError();
}
const oldName = this._name;
const oldIndexNames = [...this.objectStore.indexNames];
this._name = name;
this._rawIndex.name = name;
this.objectStore._indexesCache.delete(oldName);
this.objectStore._indexesCache.set(name, this);
this.objectStore._rawObjectStore.rawIndexes.delete(oldName);
this.objectStore._rawObjectStore.rawIndexes.set(name, this._rawIndex);
this.objectStore.indexNames = new FakeDOMStringList(...Array.from(this.objectStore._rawObjectStore.rawIndexes.keys()).filter(indexName => {
const index = this.objectStore._rawObjectStore.rawIndexes.get(indexName);
return index && !index.deleted;
}).sort());
// https://www.w3.org/TR/IndexedDB/#abort-an-upgrade-transaction - "If handle’s index was not newly created during transaction, set handle’s name to its index’s name."
if (!this.objectStore.transaction._createdIndexes.has(this._rawIndex)) {
transaction._rollbackLog.push(() => {
this._name = oldName;
this._rawIndex.name = oldName;
this.objectStore._indexesCache.delete(name);
this.objectStore._indexesCache.set(oldName, this);
this.objectStore._rawObjectStore.rawIndexes.delete(name);
this.objectStore._rawObjectStore.rawIndexes.set(oldName, this._rawIndex);
this.objectStore.indexNames = new FakeDOMStringList(...oldIndexNames);
});
}
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-openCursor-IDBRequest-any-range-IDBCursorDirection-direction
openCursor(range, direction) {
confirmActiveTransaction$1(this);
if (range === null) {
range = undefined;
}
if (range !== undefined && !(range instanceof FDBKeyRange)) {
range = FDBKeyRange.only(valueToKey(range));
}
const request = new FDBRequest();
request.source = this;
request.transaction = this.objectStore.transaction;
const cursor = new FDBCursorWithValue(this, range, direction, request);
return this.objectStore.transaction._execRequestAsync({
operation: cursor._iterate.bind(cursor),
request,
source: this
});
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-openKeyCursor-IDBRequest-any-range-IDBCursorDirection-direction
openKeyCursor(range, direction) {
confirmActiveTransaction$1(this);
if (range === null) {
range = undefined;
}
if (range !== undefined && !(range instanceof FDBKeyRange)) {
range = FDBKeyRange.only(valueToKey(range));
}
const request = new FDBRequest();
request.source = this;
request.transaction = this.objectStore.transaction;
const cursor = new FDBCursor(this, range, direction, request, true);
return this.objectStore.transaction._execRequestAsync({
operation: cursor._iterate.bind(cursor),
request,
source: this
});
}
get(key) {
confirmActiveTransaction$1(this);
if (!(key instanceof FDBKeyRange)) {
key = valueToKey(key);
}
return this.objectStore.transaction._execRequestAsync({
operation: this._rawIndex.getValue.bind(this._rawIndex, key),
source: this
});
}
// http://w3c.github.io/IndexedDB/#dom-idbindex-getall
getAll(queryOrOptions, count) {
const options = extractGetAllOptions(queryOrOptions, count, arguments.length);
confirmActiveTransaction$1(this);
const range = valueToKeyRange(options.query);
return this.objectStore.transaction._execRequestAsync({
operation: this._rawIndex.getAllValues.bind(this._rawIndex, range, options.count, options.direction),
source: this
});
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-getKey-IDBRequest-any-key
getKey(key) {
confirmActiveTransaction$1(this);
if (!(key instanceof FDBKeyRange)) {
key = valueToKey(key);
}
return this.objectStore.transaction._execRequestAsync({
operation: this._rawIndex.getKey.bind(this._rawIndex, key),
source: this
});
}
// http://w3c.github.io/IndexedDB/#dom-idbindex-getallkeys
getAllKeys(queryOrOptions, count) {
const options = extractGetAllOptions(queryOrOptions, count, arguments.length);
confirmActiveTransaction$1(this);
const range = valueToKeyRange(options.query);
return this.objectStore.transaction._execRequestAsync({
operation: this._rawIndex.getAllKeys.bind(this._rawIndex, range, options.count, options.direction),
source: this
});
}
// https://www.w3.org/TR/IndexedDB/#dom-idbobjectstore-getallrecords
getAllRecords(options) {
let query;
let count;
let direction;
if (options !== undefined) {
if (options.query !== undefined) {
query = options.query;
}
if (options.count !== undefined) {
count = enforceRange(options.count, "unsigned long");
}
if (options.direction !== undefined) {
direction = options.direction;
}
}
confirmActiveTransaction$1(this);
const range = valueToKeyRange(query);
return this.objectStore.transaction._execRequestAsync({
operation: this._rawIndex.getAllRecords.bind(this._rawIndex, range, count, direction),
source: this
});
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-count-IDBRequest-any-key
count(key) {
confirmActiveTransaction$1(this);
if (key === null) {
key = undefined;
}
if (key !== undefined && !(key instanceof FDBKeyRange)) {
key = FDBKeyRange.only(valueToKey(key));
}
return this.objectStore.transaction._execRequestAsync({
operation: () => {
return this._rawIndex.count(key);
},
source: this
});
}
get [Symbol.toStringTag]() {
return "IDBIndex";
}
}// http://w3c.github.io/IndexedDB/#check-that-a-key-could-be-injected-into-a-value
const canInjectKey = (keyPath, value) => {
if (Array.isArray(keyPath)) {
throw new Error("The key paths used in this section are always strings and never sequences, since it is not possible to create a object store which has a key generator and also has a key path that is a sequence.");
}
const identifiers = keyPath.split(".");
if (identifiers.length === 0) {
throw new Error("Assert: identifiers is not empty");
}
identifiers.pop();
for (const identifier of identifiers) {
if (typeof value !== "object" && !Array.isArray(value)) {
return false;
}
const hop = Object.hasOwn(value, identifier);
if (!hop) {
return true;
}
value = value[identifier];
}
return typeof value === "object" || Array.isArray(value);
};class FDBRecord {
constructor(key, primaryKey, value) {
this._key = key;
this._primaryKey = primaryKey;
this._value = value;
}
get key() {
return this._key;
}
set key(_) {
/* for babel */
}
get primaryKey() {
return this._primaryKey;
}
set primaryKey(_) {
/* for babel */
}
get value() {
return this._value;
}
set value(_) {
/* for babel */
}
get [Symbol.toStringTag]() {
return "IDBRecord";
}
}// what fraction of the total number of nodes are allowed to be deleted tombstones?
const MAX_TOMBSTONE_FACTOR = 2 / 3;
const EVERYTHING_KEY_RANGE = new FDBKeyRange(undefined, undefined, false, false);
/**
* Simple red-black binary tree with some aspects of a scapegoat tree. The main goal here is simplicity of
* implementation, tailored to the needs of IndexedDB.
*
* Basically this implements a [red-black tree][1] for insertions, but uses the much simpler [scapegoat tree][2]
* strategy for deletions. Deletions are a simple matter of rebuilding the tree from scratch if more than 2/3 of the
* tree is full of deleted (tombstone) markers.
*
* [1]: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree
* [2]: https://en.wikipedia.org/wiki/Scapegoat_tree
*/
class BinarySearchTree {
_numTombstones = 0;
_numNodes = 0;
/**
*
* @param keysAreUnique - whether keys can be unique, and thus whether we cn skip checking `record.value` when
* comparing. This is basically used to distinguish ObjectStores (where the value is the entire object, not used
* as a key) from non-unique Indexes (where both the key and the value are meaningful keys used for sorting)
*/
constructor(keysAreUnique) {
this._keysAreUnique = !!keysAreUnique;
}
size() {
return this._numNodes - this._numTombstones;
}
get(record) {
return this._getByComparator(this._root, otherRecord => this._compare(record, otherRecord));
}
contains(record) {
return !!this.get(record);
}
_compare(a, b) {
const keyComparison = cmp(a.key, b.key);
if (keyComparison !== 0) {
return keyComparison;
}
// if keys are unique, then we can (and must) avoid comparing the values, since they may be non-comparable
// (e.g. in the case of an ObjectStore, they are record objects)
return this._keysAreUnique ? 0 : cmp(a.value, b.value);
}
_getByComparator(node, comparator) {
let current = node;
while (current) {
const comparison = comparator(current.record);
if (comparison < 0) {
current = current.left;
} else if (comparison > 0) {
current = current.right;
} else {
return current.record;
}
}
}
/**
* Put a new record, and return the overwritten record if an overwrite occurred.
* @param record
* @param noOverwrite - throw a ConstraintError in case of overwrite
*/
put(record, noOverwrite = false) {
if (!this._root) {
this._root = {
record,
left: undefined,
right: undefined,
parent: undefined,
deleted: false,
// the root is always black in a red-black tree
red: false
};
this._numNodes++;
return;
}
return this._put(this._root, record, noOverwrite);
}
_put(node, record, noOverwrite) {
const comparison = this._compare(record, node.record);
if (comparison < 0) {
if (node.left) {
return this._put(node.left, record, noOverwrite);
} else {
node.left = {
record,
left: undefined,
right: undefined,
parent: node,
deleted: false,
red: true
};
this._onNewNodeInserted(node.left);
}
} else if (comparison > 0) {
if (node.right) {
return this._put(node.right, record, noOverwrite);
} else {
node.right = {
record,
left: undefined,
right: undefined,
parent: node,
deleted: false,
red: true
};
this._onNewNodeInserted(node.right);
}
} else if (node.deleted) {
// undelete
node.deleted = false;
node.record = record;
this._numTombstones--;
} else if (noOverwrite) {
// replace not allowed in case of noOverwrite
throw new ConstraintError();
} else {
// replace, don't add, so no need to increment. return the overwritten record
const overwrittenRecord = node.record;
node.record = record;
return overwrittenRecord;
}
}
delete(record) {
if (!this._root) {
return;
}
this._delete(this._root, record);
if (this._numTombstones > this._numNodes * MAX_TOMBSTONE_FACTOR) {
// to keep the implementation simple, and because most users of fake-indexeddb are not going to be deleting
// a lot of nodes, just rebuild the whole tree (defragment) if the tree is too full of tombstones,
// as inspired by the scapegoat tree: https://en.wikipedia.org/wiki/Scapegoat_tree#Deletion
const records = [...this.getAllRecords()];
this._root = this._rebuild(records, undefined, false);
this._numNodes = records.length;
this._numTombstones = 0;
}
}
_delete(node, record) {
if (!node) {
return;
}
const comparison = this._compare(record, node.record);
if (comparison < 0) {
this._delete(node.left, record);
} else if (comparison > 0) {
this._delete(node.right, record);
} else if (!node.deleted) {
this._numTombstones++;
node.deleted = true;
}
}
*getAllRecords(descending = false) {
yield* this.getRecords(EVERYTHING_KEY_RANGE, descending);
}
*getRecords(keyRange, descending = false) {
yield* this._getRecordsForNode(this._root, keyRange, descending);
}
*_getRecordsForNode(node, keyRange, descending = false) {
if (!node) {
return;
}
yield* this._findRecords(node, keyRange, descending);
}
*_findRecords(node, keyRange, descending = false) {
const {
lower,
upper,
lowerOpen,
upperOpen
} = keyRange;
const {
record: {
key
}
} = node;
const lowerComparison = lower === undefined ? -1 : cmp(lower, key);
const upperComparison = upper === undefined ? 1 : cmp(upper, key);
// if keys are non-unique then we need to go left/right even for equality
// else we can just do LT/GT rather than LTE/GTE as a slight optimization
const moreLeft = this._keysAreUnique ? lowerComparison < 0 : lowerComparison <= 0;
const moreRight = this._keysAreUnique ? upperComparison > 0 : upperComparison >= 0;
// in descending mode we start with rightmost nodes, else leftmost
const moreStart = descending ? moreRight : moreLeft;
const moreEnd = descending ? moreLeft : moreRight;
const start = descending ? "right" : "left";
const end = descending ? "left" : "right";
// does the current record actually match the key range?
const lowerMatches = lowerOpen ? lowerComparison < 0 : lowerComparison <= 0;
const upperMatches = upperOpen ? upperComparison > 0 : upperComparison >= 0;
if (moreStart && node[start]) {
yield* this._findRecords(node[start], keyRange, descending);
}
if (lowerMatches && upperMatches && !node.deleted) {
yield node.record;
}
if (moreEnd && node[end]) {
yield* this._findRecords(node[end], keyRange, descending);
}
}
_onNewNodeInserted(newNode) {
this._numNodes++;
this._rebalanceTree(newNode);
}
// based on https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
_rebalanceTree(node) {
let parent = node.parent;
do {
// case 1 - no red/black violation
if (!parent.red) {
return;
}
const grandparent = parent.parent;
if (!grandparent) {
// case #4 - parent is the red root, node is also red, so parent goes black
parent.red = false;
return;
}
const parentIsRightChild = parent === grandparent.right;
const uncle = parentIsRightChild ? grandparent.left : grandparent.right;
if (!uncle || !uncle.red) {
if (node === (parentIsRightChild ? parent.left : parent.right)) {
// case #5 - parent is red but uncle is black
this._rotateSubtree(parent, parentIsRightChild);
node = parent;
parent = parentIsRightChild ? grandparent.right : grandparent.left;
}
// case #6 - node is "outer" grandchild of grandparent
this._rotateSubtree(grandparent, !parentIsRightChild);
parent.red = false;
grandparent.red = true;
return;
}
// case #2 - parent and uncle are both red, so both of them go black and grandparent goes red
parent.red = false;
uncle.red = false;
grandparent.red = true;
node = grandparent;
} while (node.parent ? parent = node.parent : false);
// case #3 - current node is the root, all constraints satisfied
}
// based on https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Implementation
_rotateSubtree(node, right) {
const parent = node.parent;
const newRoot = right ? node.left : node.right; // opposite direction
const newChild = right ? newRoot.right : newRoot.left;
node[right ? "left" : "right"] = newChild;
if (newChild) {
newChild.parent = node;
}
newRoot[right ? "right" : "left"] = node;
newRoot.parent = parent;
node.parent = newRoot;
if (parent) {
parent[node === parent.right ? "right" : "left"] = newRoot;
} else {
this._root = newRoot;
}
return newRoot;
}
// rebuild the whole tree from scratch, used to avoid too many deletion tombstones accumulating
_rebuild(records, parent, red) {
const {
length
} = records;
if (!length) {
return undefined;
}
const mid = length >>> 1; // like Math.floor(records.length / 2) but fast
const node = {
record: records[mid],
left: undefined,
right: undefined,
parent,
deleted: false,
red
};
const left = this._rebuild(records.slice(0, mid), node, !red);
const right = this._rebuild(records.slice(mid + 1), node, !red);
node.left = left;
node.right = right;
return node;
}
}class RecordStore {
constructor(keysAreUnique) {
this.keysAreUnique = keysAreUnique;
this.records = new BinarySearchTree(this.keysAreUnique);
}
get(key) {
const range = key instanceof FDBKeyRange ? key : FDBKeyRange.only(key);
return this.records.getRecords(range).next().value;
}
/**
* Put a new record, and return the overwritten record if an overwrite occurred.
* @param newRecord
* @param noOverwrite - throw a ConstraintError in case of overwrite
*/
put(newRecord, noOverwrite = false) {
return this.records.put(newRecord, noOverwrite);
}
delete(key) {
const range = key instanceof FDBKeyRange ? key : FDBKeyRange.only(key);
const deletedRecords = [...this.records.getRecords(range)];
for (const record of deletedRecords) {
this.records.delete(record);
}
return deletedRecords;
}
deleteByValue(key) {
const range = key instanceof FDBKeyRange ? key : FDBKeyRange.only(key);
const deletedRecords = [];
for (const record of this.records.getAllRecords()) {
if (range.includes(record.value)) {
this.records.delete(record);
deletedRecords.push(record);
}
}
return deletedRecords;
}
clear() {
const deletedRecords = [...this.records.getAllRecords()];
this.records = new BinarySearchTree(this.keysAreUnique);
return deletedRecords;
}
values(range, direction = "next") {
const descending = direction === "prev" || direction === "prevunique";
const records = range ? this.records.getRecords(range, descending) : this.records.getAllRecords(descending);
return {
[Symbol.iterator]: () => {
const next = () => {
return records.next();
};
if (direction === "next" || direction === "prev") {
return {
next
};
}
// For nextunique/prevunique, return an iterator that skips seen values
// Note that we must return the _lowest_ value regardless of direction:
// > Iterating with "prevunique" visits the same records that "nextunique"
// > visits, but in reverse order.
// https://w3c.github.io/IndexedDB/#dom-idbcursordirection-prevunique
if (direction === "nextunique") {
let previousValue = undefined;
return {
next: () => {
let current = next();
// for nextunique, continue if we already emitted the lowest unique value
while (!current.done && previousValue !== undefined && cmp(previousValue.key, current.value.key) === 0) {
current = next();
}
previousValue = current.value;
return current;
}
};
}
// prevunique is a bit more complex due to needing to check the next value, which
// invokes the iterable, so we need to keep a buffer of one "lookahead" result
let current = next();
let nextResult = next();
return {
next: () => {
while (!nextResult.done && cmp(current.value.key, nextResult.value.key) === 0) {
// note we return the _lowest_ possible value, hence set the current
current = nextResult;
nextResult = next();
}
const result = current;
current = nextResult;
nextResult = next();
return result;
}
};
}
};
}
size() {
return this.records.size();
}
}// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-index
class Index {
deleted = false;
// Initialized should be used to decide whether to throw an error or abort the versionchange transaction when there is a
// constraint
initialized = false;
constructor(rawObjectStore, name, keyPath, multiEntry, unique) {
this.rawObjectStore = rawObjectStore;
this.name = name;
this.keyPath = keyPath;
this.multiEntry = multiEntry;
this.unique = unique;
this.records = new RecordStore(unique);
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-retrieving-a-value-from-an-index
getKey(key) {
const record = this.records.get(key);
return record !== undefined ? record.value : undefined;
}
// http://w3c.github.io/IndexedDB/#retrieve-multiple-referenced-values-from-an-index
getAllKeys(range, count, direction) {
if (count === undefined || count === 0) {
count = Infinity;
}
const records = [];
for (const record of this.records.values(range, direction)) {
records.push(structuredClone(record.value));
if (records.length >= count) {
break;
}
}
return records;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#index-referenced-value-retrieval-operation
getValue(key) {
const record = this.records.get(key);
return record !== undefined ? this.rawObjectStore.getValue(record.value) : undefined;
}
// http://w3c.github.io/IndexedDB/#retrieve-multiple-referenced-values-from-an-index
getAllValues(range, count, direction) {
if (count === undefined || count === 0) {
count = Infinity;
}
const records = [];
for (const record of this.records.values(range, direction)) {
records.push(this.rawObjectStore.getValue(record.value));
if (records.length >= count) {
break;
}
}
return records;
}
// https://www.w3.org/TR/IndexedDB/#dom-idbindex-getallrecords
getAllRecords(range, count, direction) {
if (count === undefined || count === 0) {
count = Infinity;
}
const records = [];
for (const record of this.records.values(range, direction)) {
records.push(new FDBRecord(structuredClone(record.key), structuredClone(this.rawObjectStore.getKey(record.value)), this.rawObjectStore.getValue(record.value)));
if (records.length >= count) {
break;
}
}
return records;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-storing-a-record-into-an-object-store (step 7)
storeRecord(newRecord) {
let indexKey;
try {
indexKey = extractKey(this.keyPath, newRecord.value).key;
} catch (err) {
if (err.name === "DataError") {
// Invalid key is not an actual error, just means we do not store an entry in this index
return;
}
throw err;
}
if (!this.multiEntry || !Array.isArray(indexKey)) {
try {
valueToKey(indexKey);
} catch (e) {
return;
}
} else {
// remove any elements from index key that are not valid keys and remove any duplicate elements from index
// key such that only one instance of the duplicate value remains.
const keep = [];
for (const part of indexKey) {
if (keep.indexOf(part) < 0) {
try {
keep.push(valueToKey(part));
} catch (err) {
/* Do nothing */
}
}
}
indexKey = keep;
}
if (!this.multiEntry || !Array.isArray(indexKey)) {
if (this.unique) {
const existingRecord = this.records.get(indexKey);
if (existingRecord) {
throw new ConstraintError();
}
}
} else {
if (this.unique) {
for (const individualIndexKey of indexKey) {
const existingRecord = this.records.get(individualIndexKey);
if (existingRecord) {
throw new ConstraintError();
}
}
}
}
if (!this.multiEntry || !Array.isArray(indexKey)) {
this.records.put({
key: indexKey,
value: newRecord.key
});
} else {
for (const individualIndexKey of indexKey) {
this.records.put({
key: individualIndexKey,
value: newRecord.key
});
}
}
}
initialize(transaction) {
if (this.initialized) {
throw new Error("Index already initialized");
}
transaction._execRequestAsync({
operation: () => {
try {
// Create index based on current value of objectstore
for (const record of this.rawObjectStore.records.values()) {
this.storeRecord(record);
}
this.initialized = true;
} catch (err) {
// console.error(err);
transaction._abort(err.name);
}
},
source: null
});
}
count(range) {
let count = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const record of this.records.values(range)) {
count += 1;
}
return count;
}
}// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-valid-key-path
const validateKeyPath = (keyPath, parent) => {
// This doesn't make sense to me based on the spec, but it is needed to pass the W3C KeyPath tests (see same
// comment in extractKey)
if (keyPath !== undefined && keyPath !== null && typeof keyPath !== "string" && keyPath.toString && (parent === "array" || !Array.isArray(keyPath))) {
keyPath = keyPath.toString();
}
if (typeof keyPath === "string") {
if (keyPath === "" && parent !== "string") {
return;
}
try {
// https://mathiasbynens.be/demo/javascript-identifier-regex for ECMAScript 5.1 / Unicode v7.0.0, with
// reserved words at beginning removed
const validIdentifierRegex =
// eslint-disable-next-line no-misleading-character-class
/^(?:[$A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B2\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])(?:[$0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B2\u08E4-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA69D\uA69F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2D\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])*$/;
if (keyPath.length >= 1 && validIdentifierRegex.test(keyPath)) {
return;
}
} catch (err) {
throw new SyntaxError(err.message);
}
if (keyPath.indexOf(" ") >= 0) {
throw new SyntaxError("The keypath argument contains an invalid key path (no spaces allowed).");
}
}
if (Array.isArray(keyPath) && keyPath.length > 0) {
if (parent) {
// No nested arrays
throw new SyntaxError("The keypath argument contains an invalid key path (nested arrays).");
}
for (const part of keyPath) {
validateKeyPath(part, "array");
}
return;
} else if (typeof keyPath === "string" && keyPath.indexOf(".") >= 0) {
keyPath = keyPath.split(".");
for (const part of keyPath) {
validateKeyPath(part, "string");
}
return;
}
throw new SyntaxError();
};const confirmActiveTransaction = objectStore => {
if (objectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (objectStore.transaction._state !== "active") {
throw new TransactionInactiveError();
}
};
const buildRecordAddPut = (objectStore, value, key) => {
confirmActiveTransaction(objectStore);
if (objectStore.transaction.mode === "readonly") {
throw new ReadOnlyError();
}
if (objectStore.keyPath !== null) {
if (key !== undefined) {
throw new DataError();
}
}
const clone = cloneValueForInsertion(value, objectStore.transaction);
if (objectStore.keyPath !== null) {
const tempKey = extractKey(objectStore.keyPath, clone);
if (tempKey.type === "found") {
valueToKey(tempKey.key);
} else {
if (!objectStore._rawObjectStore.keyGenerator) {
throw new DataError();
} else if (!canInjectKey(objectStore.keyPath, clone)) {
throw new DataError();
}
}
}
if (objectStore.keyPath === null && objectStore._rawObjectStore.keyGenerator === null && key === undefined) {
throw new DataError();
}
if (key !== undefined) {
key = valueToKey(key);
}
return {
key,
value: clone
};
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#object-store
class FDBObjectStore {
_indexesCache = new Map();
constructor(transaction, rawObjectStore) {
this._rawObjectStore = rawObjectStore;
this._name = rawObjectStore.name;
this.keyPath = getKeyPath(rawObjectStore.keyPath);
this.autoIncrement = rawObjectStore.autoIncrement;
this.transaction = transaction;
this.indexNames = new FakeDOMStringList(...Array.from(rawObjectStore.rawIndexes.keys()).sort());
}
get name() {
return this._name;
}
// http://w3c.github.io/IndexedDB/#dom-idbobjectstore-name
set name(name) {
const transaction = this.transaction;
if (!transaction.db._runningVersionchangeTransaction) {
throw transaction._state === "active" ? new InvalidStateError() : new TransactionInactiveError();
}
confirmActiveTransaction(this);
name = String(name);
if (name === this._name) {
return;
}
if (this._rawObjectStore.rawDatabase.rawObjectStores.has(name)) {
throw new ConstraintError();
}
const oldName = this._name;
const oldObjectStoreNames = [...transaction.db.objectStoreNames];
this._name = name;
this._rawObjectStore.name = name;
this.transaction._objectStoresCache.delete(oldName);
this.transaction._objectStoresCache.set(name, this);
this._rawObjectStore.rawDatabase.rawObjectStores.delete(oldName);
this._rawObjectStore.rawDatabase.rawObjectStores.set(name, this._rawObjectStore);
transaction.db.objectStoreNames = new FakeDOMStringList(...Array.from(this._rawObjectStore.rawDatabase.rawObjectStores.keys()).filter(objectStoreName => {
const objectStore = this._rawObjectStore.rawDatabase.rawObjectStores.get(objectStoreName);
return objectStore && !objectStore.deleted;
}).sort());
const oldScope = new Set(transaction._scope);
const oldTransactionObjectStoreNames = [...transaction.objectStoreNames];
this.transaction._scope.delete(oldName);
transaction._scope.add(name);
transaction.objectStoreNames = new FakeDOMStringList(...Array.from(transaction._scope).sort());
// https://www.w3.org/TR/IndexedDB/#abort-an-upgrade-transaction - "If handle’s object store was not newly created during transaction, set handle’s name to its object store’s name."
if (!this.transaction._createdObjectStores.has(this._rawObjectStore)) {
transaction._rollbackLog.push(() => {
this._name = oldName;
this._rawObjectStore.name = oldName;
this.transaction._objectStoresCache.delete(name);
this.transaction._objectStoresCache.set(oldName, this);
this._rawObjectStore.rawDatabase.rawObjectStores.delete(name);
this._rawObjectStore.rawDatabase.rawObjectStores.set(oldName, this._rawObjectStore);
transaction.db.objectStoreNames = new FakeDOMStringList(...oldObjectStoreNames);
transaction._scope = oldScope;
transaction.objectStoreNames = new FakeDOMStringList(...oldTransactionObjectStoreNames);
});
}
}
put(value, key) {
if (arguments.length === 0) {
throw new TypeError();
}
const record = buildRecordAddPut(this, value, key);
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.storeRecord.bind(this._rawObjectStore, record, false, this.transaction._rollbackLog),
source: this
});
}
add(value, key) {
if (arguments.length === 0) {
throw new TypeError();
}
const record = buildRecordAddPut(this, value, key);
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.storeRecord.bind(this._rawObjectStore, record, true, this.transaction._rollbackLog),
source: this
});
}
delete(key) {
if (arguments.length === 0) {
throw new TypeError();
}
confirmActiveTransaction(this);
if (this.transaction.mode === "readonly") {
throw new ReadOnlyError();
}
if (!(key instanceof FDBKeyRange)) {
key = valueToKey(key);
}
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.deleteRecord.bind(this._rawObjectStore, key, this.transaction._rollbackLog),
source: this
});
}
get(key) {
if (arguments.length === 0) {
throw new TypeError();
}
confirmActiveTransaction(this);
if (!(key instanceof FDBKeyRange)) {
key = valueToKey(key);
}
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.getValue.bind(this._rawObjectStore, key),
source: this
});
}
// http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getall
getAll(queryOrOptions, count) {
const options = extractGetAllOptions(queryOrOptions, count, arguments.length);
confirmActiveTransaction(this);
const range = valueToKeyRange(options.query);
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.getAllValues.bind(this._rawObjectStore, range, options.count, options.direction),
source: this
});
}
// http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getkey
getKey(key) {
if (arguments.length === 0) {
throw new TypeError();
}
confirmActiveTransaction(this);
if (!(key instanceof FDBKeyRange)) {
key = valueToKey(key);
}
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.getKey.bind(this._rawObjectStore, key),
source: this
});
}
// http://w3c.github.io/IndexedDB/#dom-idbobjectstore-getallkeys
getAllKeys(queryOrOptions, count) {
const options = extractGetAllOptions(queryOrOptions, count, arguments.length);
confirmActiveTransaction(this);
const range = valueToKeyRange(options.query);
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.getAllKeys.bind(this._rawObjectStore, range, options.count, options.direction),
source: this
});
}
// https://www.w3.org/TR/IndexedDB/#dom-idbobjectstore-getallrecords
getAllRecords(options) {
let query;
let count;
let direction;
if (options !== undefined) {
if (options.query !== undefined) {
query = options.query;
}
if (options.count !== undefined) {
count = enforceRange(options.count, "unsigned long");
}
if (options.direction !== undefined) {
direction = options.direction;
}
}
confirmActiveTransaction(this);
const range = valueToKeyRange(query);
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.getAllRecords.bind(this._rawObjectStore, range, count, direction),
source: this
});
}
clear() {
confirmActiveTransaction(this);
if (this.transaction.mode === "readonly") {
throw new ReadOnlyError();
}
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.clear.bind(this._rawObjectStore, this.transaction._rollbackLog),
source: this
});
}
openCursor(range, direction) {
confirmActiveTransaction(this);
if (range === null) {
range = undefined;
}
if (range !== undefined && !(range instanceof FDBKeyRange)) {
range = FDBKeyRange.only(valueToKey(range));
}
const request = new FDBRequest();
request.source = this;
request.transaction = this.transaction;
const cursor = new FDBCursorWithValue(this, range, direction, request);
return this.transaction._execRequestAsync({
operation: cursor._iterate.bind(cursor),
request,
source: this
});
}
openKeyCursor(range, direction) {
confirmActiveTransaction(this);
if (range === null) {
range = undefined;
}
if (range !== undefined && !(range instanceof FDBKeyRange)) {
range = FDBKeyRange.only(valueToKey(range));
}
const request = new FDBRequest();
request.source = this;
request.transaction = this.transaction;
const cursor = new FDBCursor(this, range, direction, request, true);
return this.transaction._execRequestAsync({
operation: cursor._iterate.bind(cursor),
request,
source: this
});
}
// tslint:-next-line max-line-length
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBObjectStore-createIndex-IDBIndex-DOMString-name-DOMString-sequence-DOMString--keyPath-IDBIndexParameters-optionalParameters
createIndex(name, keyPath, optionalParameters = {}) {
if (arguments.length < 2) {
throw new TypeError();
}
const multiEntry = optionalParameters.multiEntry !== undefined ? optionalParameters.multiEntry : false;
const unique = optionalParameters.unique !== undefined ? optionalParameters.unique : false;
if (this.transaction.mode !== "versionchange") {
throw new InvalidStateError();
}
confirmActiveTransaction(this);
if (this.indexNames.contains(name)) {
throw new ConstraintError();
}
validateKeyPath(keyPath);
if (Array.isArray(keyPath) && multiEntry) {
throw new InvalidAccessError();
}
// The index that is requested to be created can contain constraints on the data allowed in the index's
// referenced object store, such as requiring uniqueness of the values referenced by the index's keyPath. If the
// referenced object store already contains data which violates these constraints, this MUST NOT cause the
// implementation of createIndex to throw an exception or affect what it returns. The implementation MUST still
// create and return an IDBIndex object. Instead the implementation must queue up an operation to abort the
// "versionchange" transaction which was used for the createIndex call.
// Save for rollbackLog
const indexNames = [...this.indexNames];
const index = new Index(this._rawObjectStore, name, keyPath, multiEntry, unique);
this.indexNames._push(name);
this.indexNames._sort();
this.transaction._createdIndexes.add(index);
this._rawObjectStore.rawIndexes.set(name, index);
index.initialize(this.transaction); // This is async by design
this.transaction._rollbackLog.push(() => {
index.deleted = true;
this.indexNames = new FakeDOMStringList(...indexNames);
this._rawObjectStore.rawIndexes.delete(index.name);
});
return new FDBIndex(this, index);
}
// https://w3c.github.io/IndexedDB/#dom-idbobjectstore-index
index(name) {
if (arguments.length === 0) {
throw new TypeError();
}
if (this._rawObjectStore.deleted || this.transaction._state === "finished") {
throw new InvalidStateError();
}
const index = this._indexesCache.get(name);
if (index !== undefined) {
return index;
}
const rawIndex = this._rawObjectStore.rawIndexes.get(name);
if (!this.indexNames.contains(name) || rawIndex === undefined) {
throw new NotFoundError();
}
const index2 = new FDBIndex(this, rawIndex);
this._indexesCache.set(name, index2);
return index2;
}
deleteIndex(name) {
if (arguments.length === 0) {
throw new TypeError();
}
if (this.transaction.mode !== "versionchange") {
throw new InvalidStateError();
}
confirmActiveTransaction(this);
const rawIndex = this._rawObjectStore.rawIndexes.get(name);
if (rawIndex === undefined) {
throw new NotFoundError();
}
this.transaction._rollbackLog.push(() => {
rawIndex.deleted = false;
this._rawObjectStore.rawIndexes.set(rawIndex.name, rawIndex);
this.indexNames._push(rawIndex.name);
this.indexNames._sort();
});
this.indexNames = new FakeDOMStringList(...Array.from(this.indexNames).filter(indexName => {
return indexName !== name;
}));
rawIndex.deleted = true; // Not sure if this is supposed to happen synchronously
this.transaction._execRequestAsync({
operation: () => {
const rawIndex2 = this._rawObjectStore.rawIndexes.get(name);
// Hack in case another index is given this name before this async request is processed. It'd be better
// to have a real unique ID for each index.
if (rawIndex === rawIndex2) {
this._rawObjectStore.rawIndexes.delete(name);
}
},
source: this
});
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBObjectStore-count-IDBRequest-any-key
count(key) {
confirmActiveTransaction(this);
if (key === null) {
key = undefined;
}
if (key !== undefined && !(key instanceof FDBKeyRange)) {
key = FDBKeyRange.only(valueToKey(key));
}
return this.transaction._execRequestAsync({
operation: () => {
return this._rawObjectStore.count(key);
},
source: this
});
}
get [Symbol.toStringTag]() {
return "IDBObjectStore";
}
}class Event {
eventPath = [];
NONE = 0;
CAPTURING_PHASE = 1;
AT_TARGET = 2;
BUBBLING_PHASE = 3;
// Flags
propagationStopped = false;
immediatePropagationStopped = false;
canceled = false;
initialized = true;
dispatched = false;
target = null;
currentTarget = null;
eventPhase = 0;
defaultPrevented = false;
isTrusted = false;
timeStamp = Date.now();
constructor(type, eventInitDict = {}) {
this.type = type;
this.bubbles = eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false;
this.cancelable = eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false;
}
preventDefault() {
if (this.cancelable) {
this.canceled = true;
}
}
stopPropagation() {
this.propagationStopped = true;
}
stopImmediatePropagation() {
this.propagationStopped = true;
this.immediatePropagationStopped = true;
}
}// When running within Node.js (including jsdom), we want to use setImmediate
// (which runs immediately) rather than setTimeout (which enforces a minimum
// delay of 1ms, and on Windows only has a resolution of 15ms or so). jsdom
// doesn't provide setImmediate (to better match the browser environment) and
// sandboxes scripts, but its sandbox is by necessity imperfect, so we can break
// out of it:
//
// - https://github.com/jsdom/jsdom#executing-scripts
// - https://github.com/jsdom/jsdom/issues/2729
// - https://github.com/scala-js/scala-js-macrotask-executor/pull/17
function getSetImmediateFromJsdom() {
if (typeof navigator !== "undefined" && /jsdom/.test(navigator.userAgent)) {
const outerRealmFunctionConstructor = Node.constructor;
return new outerRealmFunctionConstructor("return setImmediate")();
} else {
return undefined;
}
}
// waiting on this PR for typescript types: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1249
// 'postTask' runs right after microtasks, so equivalent to setTimeout but without the 4ms clamping.
// Using the default priority of 'user-visible' to avoid blocking input while still running fairly quickly.
// See: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API#task_priorities
const schedulerPostTask = typeof scheduler !== "undefined" && (fn => scheduler.postTask(fn));
// fallback for environments that don't support any of the above
const doSetTimeout = fn => setTimeout(fn, 0);
// Schedules a task to run later. Use Node.js's setImmediate if available and
// setTimeout otherwise. Note that options like process.nextTick or
// queueMicrotask will likely not work: IndexedDB semantics require that
// transactions are marked as not active when the event loop runs. The next
// tick queue and microtask queue run within the current event loop macrotask,
// so they'd process database operations too quickly.
const queueTask = fn => {
const setImmediate = globalThis.setImmediate || getSetImmediateFromJsdom() || schedulerPostTask || doSetTimeout;
setImmediate(fn);
};const prioritizedListenerTypes = ["error", "abort", "complete"];
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction
class FDBTransaction extends FakeEventTarget {
_state = "active";
_started = false;
_rollbackLog = [];
_objectStoresCache = new Map();
_openRequest = null;
error = null;
onabort = null;
oncomplete = null;
onerror = null;
_prioritizedListeners = new Map();
_requests = [];
_createdIndexes = new Set();
_createdObjectStores = new Set();
constructor(storeNames, mode, durability, db) {
super();
this._scope = new Set(storeNames);
this.mode = mode;
this.durability = durability;
this.db = db;
this.objectStoreNames = new FakeDOMStringList(...Array.from(this._scope).sort());
for (const type of prioritizedListenerTypes) {
// Attach prioritized (internal) listeners before any external listeners are attached.
// This ensures that these listeners run with the same timing regardless of whether
// the user uses `on*` or `addEventListener` for event listeners.
this.addEventListener(type, () => {
this._prioritizedListeners.get(type)?.();
});
}
}
// https://w3c.github.io/IndexedDB/#abort-transaction
_abort(errName) {
for (const f of this._rollbackLog.reverse()) {
f();
}
if (errName !== null) {
const e = new DOMException(undefined, errName);
this.error = e;
}
// Should this directly remove from _requests?
for (const {
request
} of this._requests) {
if (request.readyState !== "done") {
request.readyState = "done"; // This will cancel execution of this request's operation
if (request.source) {
// https://w3c.github.io/IndexedDB/#ref-for-list-iterate%E2%91%A2
// For each request of transaction’s request list, abort the steps to asynchronously
// execute a request for request, set request’s processed flag to true, and queue a
// database task to run these steps:
queueTask(() => {
// Set request’s result to undefined.
request.result = undefined;
// Set request’s error to a newly created "AbortError" DOMException.
request.error = new AbortError();
// Fire an event named error at request with its bubbles and cancelable attributes initialized
// to true.
const event = new Event("error", {
bubbles: true,
cancelable: true
});
event.eventPath = [this.db, this];
try {
request.dispatchEvent(event);
} catch (_err) {
if (this._state === "active") {
this._abort("AbortError");
}
}
});
}
}
}
// Queue a database task to run these steps:
queueTask(() => {
// If transaction is an upgrade transaction, then set transaction’s connection’s associated database’s
// upgrade transaction to null.
// (i.e. remove it from the list of `db.connections`)
const isUpgradeTransaction = this.mode === "versionchange";
if (isUpgradeTransaction) {
this.db._rawDatabase.connections = this.db._rawDatabase.connections.filter(connection => !connection._rawDatabase.transactions.includes(this));
}
// Fire an event named abort at transaction with its bubbles attribute initialized to true.
const event = new Event("abort", {
bubbles: true,
cancelable: false
});
event.eventPath = [this.db];
this.dispatchEvent(event);
// If transaction is an upgrade transaction, then:
if (isUpgradeTransaction) {
// Let request be the open request associated with transaction.
const request = this._openRequest;
// Set request’s transaction to null.
request.transaction = null;
// Set request’s result to undefined.
request.result = undefined;
}
});
this._state = "finished";
}
abort() {
if (this._state === "committing" || this._state === "finished") {
throw new InvalidStateError();
}
this._state = "active";
this._abort(null);
}
// http://w3c.github.io/IndexedDB/#dom-idbtransaction-objectstore
objectStore(name) {
if (this._state !== "active") {
throw new InvalidStateError();
}
const objectStore = this._objectStoresCache.get(name);
if (objectStore !== undefined) {
return objectStore;
}
const rawObjectStore = this.db._rawDatabase.rawObjectStores.get(name);
if (!this._scope.has(name) || rawObjectStore === undefined) {
throw new NotFoundError();
}
const objectStore2 = new FDBObjectStore(this, rawObjectStore);
this._objectStoresCache.set(name, objectStore2);
return objectStore2;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-asynchronously-executing-a-request
_execRequestAsync(obj) {
const source = obj.source;
const operation = obj.operation;
let request = Object.hasOwn(obj, "request") ? obj.request : null;
if (this._state !== "active") {
throw new TransactionInactiveError();
}
// Request should only be passed for cursors
if (!request) {
if (!source) {
// Special requests like indexes that just need to run some code
request = new FDBRequest();
} else {
request = new FDBRequest();
request.source = source;
request.transaction = source.transaction;
}
}
this._requests.push({
operation,
request
});
return request;
}
_start() {
this._started = true;
// Remove from request queue - cursor ones will be added back if necessary by cursor.continue and such
let operation;
let request;
while (this._requests.length > 0) {
const r = this._requests.shift();
// This should only be false if transaction was aborted
if (r && r.request.readyState !== "done") {
request = r.request;
operation = r.operation;
break;
}
}
if (request && operation) {
if (!request.source) {
// Special requests like indexes that just need to run some code, with error handling already built into
// operation
operation();
} else {
let defaultAction;
let event;
try {
const result = operation();
request.readyState = "done";
request.result = result;
request.error = undefined;
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-fire-a-success-event
if (this._state === "inactive") {
this._state = "active";
}
event = new Event("success", {
bubbles: false,
cancelable: false
});
} catch (err) {
request.readyState = "done";
request.result = undefined;
request.error = err;
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-fire-an-error-event
if (this._state === "inactive") {
this._state = "active";
}
event = new Event("error", {
bubbles: true,
cancelable: true
});
defaultAction = this._abort.bind(this, err.name);
}
try {
event.eventPath = [this.db, this];
request.dispatchEvent(event);
} catch (_err) {
if (this._state === "active") {
this._abort("AbortError");
defaultAction = undefined; // do not abort again
}
}
// Default action of event
if (!event.canceled) {
if (defaultAction) {
defaultAction();
}
}
}
// Give it another chance for new handlers to be set before finishing
queueTask(this._start.bind(this));
return;
}
// Check if transaction complete event needs to be fired
if (this._state !== "finished") {
// Either aborted or committed already
this._state = "finished";
if (!this.error) {
const event = new Event("complete");
this.dispatchEvent(event);
}
}
}
commit() {
if (this._state !== "active") {
throw new InvalidStateError();
}
this._state = "committing";
}
get [Symbol.toStringTag]() {
return "IDBTransaction";
}
}const MAX_KEY = 9007199254740992;
class KeyGenerator {
// This is kind of wrong. Should start at 1 and increment only after record is saved
num = 0;
next() {
if (this.num >= MAX_KEY) {
throw new ConstraintError();
}
this.num += 1;
return this.num;
}
// https://w3c.github.io/IndexedDB/#possibly-update-the-key-generator
setIfLarger(num) {
const value = Math.floor(Math.min(num, MAX_KEY)) - 1;
if (value >= this.num) {
this.num = value + 1;
}
}
}// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-object-store
class ObjectStore {
deleted = false;
records = new RecordStore(true);
rawIndexes = new Map();
constructor(rawDatabase, name, keyPath, autoIncrement) {
this.rawDatabase = rawDatabase;
this.keyGenerator = autoIncrement === true ? new KeyGenerator() : null;
this.deleted = false;
this.name = name;
this.keyPath = keyPath;
this.autoIncrement = autoIncrement;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-retrieving-a-value-from-an-object-store
getKey(key) {
const record = this.records.get(key);
return record !== undefined ? structuredClone(record.key) : undefined;
}
// http://w3c.github.io/IndexedDB/#retrieve-multiple-keys-from-an-object-store
getAllKeys(range, count, direction) {
if (count === undefined || count === 0) {
count = Infinity;
}
const records = [];
for (const record of this.records.values(range, direction)) {
records.push(structuredClone(record.key));
if (records.length >= count) {
break;
}
}
return records;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-retrieving-a-value-from-an-object-store
getValue(key) {
const record = this.records.get(key);
return record !== undefined ? structuredClone(record.value) : undefined;
}
// http://w3c.github.io/IndexedDB/#retrieve-multiple-values-from-an-object-store
getAllValues(range, count, direction) {
if (count === undefined || count === 0) {
count = Infinity;
}
const records = [];
for (const record of this.records.values(range, direction)) {
records.push(structuredClone(record.value));
if (records.length >= count) {
break;
}
}
return records;
}
// https://www.w3.org/TR/IndexedDB/#dom-idbobjectstore-getallrecords
getAllRecords(range, count, direction) {
if (count === undefined || count === 0) {
count = Infinity;
}
const records = [];
for (const record of this.records.values(range, direction)) {
records.push(new FDBRecord(structuredClone(record.key), structuredClone(record.key), structuredClone(record.value)));
if (records.length >= count) {
break;
}
}
return records;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-storing-a-record-into-an-object-store
storeRecord(newRecord, noOverwrite, rollbackLog) {
if (this.keyPath !== null) {
const key = extractKey(this.keyPath, newRecord.value).key;
if (key !== undefined) {
newRecord.key = key;
}
}
const rollbackLogForThisOperation = [];
if (this.keyGenerator !== null && newRecord.key === undefined) {
let rolledBack = false;
const keyGeneratorBefore = this.keyGenerator.num;
const rollbackKeyGenerator = () => {
if (rolledBack) {
return;
}
rolledBack = true;
if (this.keyGenerator) {
this.keyGenerator.num = keyGeneratorBefore;
}
};
rollbackLogForThisOperation.push(rollbackKeyGenerator);
if (rollbackLog) {
rollbackLog.push(rollbackKeyGenerator);
}
newRecord.key = this.keyGenerator.next();
// Set in value if keyPath defiend but led to no key
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-to-assign-a-key-to-a-value-using-a-key-path
if (this.keyPath !== null) {
if (Array.isArray(this.keyPath)) {
throw new Error("Cannot have an array key path in an object store with a key generator");
}
let remainingKeyPath = this.keyPath;
let object = newRecord.value;
let identifier;
let i = 0; // Just to run the loop at least once
while (i >= 0) {
if (typeof object !== "object") {
throw new DataError();
}
i = remainingKeyPath.indexOf(".");
if (i >= 0) {
identifier = remainingKeyPath.slice(0, i);
remainingKeyPath = remainingKeyPath.slice(i + 1);
if (!Object.hasOwn(object, identifier)) {
// Bypass prototype when setting (See `bindings-inject-values-bypass.any.js`)
// Equivalent to `object[identifier] = ...` without using `Object.prototype`
Object.defineProperty(object, identifier, {
configurable: true,
enumerable: true,
writable: true,
value: {}
});
}
object = object[identifier];
}
}
identifier = remainingKeyPath;
// Bypass prototype when setting (See `bindings-inject-values-bypass.any.js`)
// Equivalent to `object[identifier] = ...` without using `Object.prototype`
Object.defineProperty(object, identifier, {
configurable: true,
enumerable: true,
writable: true,
value: newRecord.key
});
}
} else if (this.keyGenerator !== null && typeof newRecord.key === "number") {
this.keyGenerator.setIfLarger(newRecord.key);
}
const existingRecord = this.records.put(newRecord, noOverwrite);
let rolledBack = false;
const rollbackStoreRecord = () => {
if (rolledBack) {
return;
}
rolledBack = true;
if (existingRecord) {
// overwrite on rollback
this.storeRecord(existingRecord, false);
} else {
// delete on rollback
this.deleteRecord(newRecord.key);
}
};
rollbackLogForThisOperation.push(rollbackStoreRecord);
if (rollbackLog) {
rollbackLog.push(rollbackStoreRecord);
}
// Delete existing indexes
if (existingRecord) {
for (const rawIndex of this.rawIndexes.values()) {
rawIndex.records.deleteByValue(newRecord.key);
}
}
// Update indexes
try {
for (const rawIndex of this.rawIndexes.values()) {
if (rawIndex.initialized) {
rawIndex.storeRecord(newRecord);
}
}
} catch (err) {
// If this request fails here and preventDefault is used to stop the transaction from aborting, we need to roll back the addition of this record to the store, otherwise it will be present in subsequent requests on this transaction. Same for key generator.
if (err.name === "ConstraintError") {
for (const rollback of rollbackLogForThisOperation) {
rollback();
}
}
throw err;
}
return newRecord.key;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-deleting-records-from-an-object-store
deleteRecord(key, rollbackLog) {
const deletedRecords = this.records.delete(key);
if (rollbackLog) {
for (const record of deletedRecords) {
rollbackLog.push(() => {
this.storeRecord(record, true);
});
}
}
for (const rawIndex of this.rawIndexes.values()) {
rawIndex.records.deleteByValue(key);
}
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-clearing-an-object-store
clear(rollbackLog) {
const deletedRecords = this.records.clear();
if (rollbackLog) {
for (const record of deletedRecords) {
rollbackLog.push(() => {
this.storeRecord(record, true);
});
}
}
for (const rawIndex of this.rawIndexes.values()) {
rawIndex.records.clear();
}
}
count(range) {
// optimization: if there is no range, or if the range is everything, then we can just count the total size
if (range === undefined || range.lower === undefined && range.upper === undefined) {
return this.records.size();
}
let count = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const record of this.records.values(range)) {
count += 1;
}
return count;
}
}// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-closing-steps
const closeConnection = (connection, forced = false) => {
connection._closePending = true;
const transactionsComplete = connection._rawDatabase.transactions.every(transaction => {
return transaction._state === "finished";
});
if (transactionsComplete) {
connection._closed = true;
connection._rawDatabase.connections = connection._rawDatabase.connections.filter(otherConnection => {
return connection !== otherConnection;
});
if (forced) {
const event = new Event("close", {
bubbles: false,
cancelable: false
});
event.eventPath = [];
connection.dispatchEvent(event);
}
} else {
queueTask(() => {
closeConnection(connection, forced);
});
}
};// Common first 3 steps of https://www.w3.org/TR/IndexedDB/#dom-idbdatabase-createobjectstore and https://www.w3.org/TR/IndexedDB/#dom-idbdatabase-deleteobjectstore
const confirmActiveVersionchangeTransaction = database => {
// Let transaction be database’s upgrade transaction if it is not null, or throw an "InvalidStateError" DOMException otherwise.
let transaction;
if (database._runningVersionchangeTransaction) {
// Find the latest versionchange transaction
transaction = database._rawDatabase.transactions.findLast(tx => {
return tx.mode === "versionchange";
});
}
if (!transaction) {
throw new InvalidStateError();
}
// If transaction’s state is not active, then throw a "TransactionInactiveError" DOMException.
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
return transaction;
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-interface
class FDBDatabase extends FakeEventTarget {
_closePending = false;
_closed = false;
_runningVersionchangeTransaction = false;
constructor(rawDatabase) {
super();
this._rawDatabase = rawDatabase;
this._rawDatabase.connections.push(this);
this.name = rawDatabase.name;
this.version = rawDatabase.version;
this.objectStoreNames = new FakeDOMStringList(...Array.from(rawDatabase.rawObjectStores.keys()).sort());
}
// http://w3c.github.io/IndexedDB/#dom-idbdatabase-createobjectstore
createObjectStore(name, options = {}) {
if (name === undefined) {
throw new TypeError();
}
const transaction = confirmActiveVersionchangeTransaction(this);
const keyPath = options !== null && options.keyPath !== undefined ? options.keyPath : null;
const autoIncrement = options !== null && options.autoIncrement !== undefined ? options.autoIncrement : false;
if (keyPath !== null) {
validateKeyPath(keyPath);
}
if (this._rawDatabase.rawObjectStores.has(name)) {
throw new ConstraintError();
}
if (autoIncrement && (keyPath === "" || Array.isArray(keyPath))) {
throw new InvalidAccessError();
}
// Save for rollbackLog
const objectStoreNames = [...this.objectStoreNames];
const transactionObjectStoreNames = [...transaction.objectStoreNames];
const rawObjectStore = new ObjectStore(this._rawDatabase, name, keyPath, autoIncrement);
this.objectStoreNames._push(name);
this.objectStoreNames._sort();
transaction._scope.add(name);
transaction._createdObjectStores.add(rawObjectStore);
this._rawDatabase.rawObjectStores.set(name, rawObjectStore);
transaction.objectStoreNames = new FakeDOMStringList(...this.objectStoreNames);
transaction._rollbackLog.push(() => {
rawObjectStore.deleted = true;
this.objectStoreNames = new FakeDOMStringList(...objectStoreNames);
transaction.objectStoreNames = new FakeDOMStringList(...transactionObjectStoreNames);
transaction._scope.delete(rawObjectStore.name);
this._rawDatabase.rawObjectStores.delete(rawObjectStore.name);
});
return transaction.objectStore(name);
}
// https://www.w3.org/TR/IndexedDB/#dom-idbdatabase-deleteobjectstore
deleteObjectStore(name) {
if (name === undefined) {
throw new TypeError();
}
const transaction = confirmActiveVersionchangeTransaction(this);
// Let store be the object store named name in database, or throw a "NotFoundError" DOMException if none.
const store = this._rawDatabase.rawObjectStores.get(name);
if (store === undefined) {
throw new NotFoundError();
}
// Remove store from this’s object store set.
// This method synchronously modifies the objectStoreNames property on the IDBDatabase instance on which it was called.
this.objectStoreNames = new FakeDOMStringList(...Array.from(this.objectStoreNames).filter(objectStoreName => {
return objectStoreName !== name;
}));
transaction.objectStoreNames = new FakeDOMStringList(...this.objectStoreNames);
// If there is an object store handle associated with store and transaction, remove all entries from its index set.
const objectStore = transaction._objectStoresCache.get(name);
let prevIndexNames;
if (objectStore) {
prevIndexNames = [...objectStore.indexNames];
objectStore.indexNames = new FakeDOMStringList();
}
transaction._rollbackLog.push(() => {
store.deleted = false;
this._rawDatabase.rawObjectStores.set(store.name, store);
this.objectStoreNames._push(store.name);
transaction.objectStoreNames._push(store.name);
this.objectStoreNames._sort();
if (objectStore && prevIndexNames) {
objectStore.indexNames = new FakeDOMStringList(...prevIndexNames);
}
});
// Destroy store.
store.deleted = true;
this._rawDatabase.rawObjectStores.delete(name);
transaction._objectStoresCache.delete(name);
}
transaction(storeNames, mode, options) {
mode = mode !== undefined ? mode : "readonly";
if (mode !== "readonly" && mode !== "readwrite" && mode !== "versionchange") {
throw new TypeError("Invalid mode: " + mode);
}
const hasActiveVersionchange = this._rawDatabase.transactions.some(transaction => {
return transaction._state === "active" && transaction.mode === "versionchange" && transaction.db === this;
});
if (hasActiveVersionchange) {
throw new InvalidStateError();
}
if (this._closePending) {
throw new InvalidStateError();
}
if (!Array.isArray(storeNames)) {
storeNames = [storeNames];
}
if (storeNames.length === 0 && mode !== "versionchange") {
throw new InvalidAccessError();
}
for (const storeName of storeNames) {
if (!this.objectStoreNames.contains(storeName)) {
throw new NotFoundError("No objectStore named " + storeName + " in this database");
}
}
// the actual algo is more complex but this passes the IDB tests: https://webidl.spec.whatwg.org/#es-dictionary
const durability = options?.durability ?? "default";
// invalid enums throw a TypeError: https://webidl.spec.whatwg.org/#es-enumeration
if (durability !== "default" && durability !== "strict" && durability !== "relaxed") {
throw new TypeError(
// based on Firefox's error message
`'${durability}' (value of 'durability' member of IDBTransactionOptions) ` + `is not a valid value for enumeration IDBTransactionDurability`);
}
const tx = new FDBTransaction(storeNames, mode, durability, this);
this._rawDatabase.transactions.push(tx);
this._rawDatabase.processTransactions(); // See if can start right away (async)
return tx;
}
close() {
closeConnection(this);
}
get [Symbol.toStringTag]() {
return "IDBDatabase";
}
}class FDBOpenDBRequest extends FDBRequest {
onupgradeneeded = null;
onblocked = null;
get [Symbol.toStringTag]() {
return "IDBOpenDBRequest";
}
}class FDBVersionChangeEvent extends Event {
constructor(type, parameters = {}) {
super(type);
this.newVersion = parameters.newVersion !== undefined ? parameters.newVersion : null;
this.oldVersion = parameters.oldVersion !== undefined ? parameters.oldVersion : 0;
}
get [Symbol.toStringTag]() {
return "IDBVersionChangeEvent";
}
}/**
* Minimal polyfill of `Set.prototype.intersection`, available in Node 22+.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/intersection
* @param set1
* @param set2
*/
function intersection(set1, set2) {
if ("intersection" in set1) {
return set1.intersection(set2);
}
return new Set([...set1].filter(item => set2.has(item)));
}// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-database
class Database {
transactions = [];
rawObjectStores = new Map();
connections = [];
constructor(name, version) {
this.name = name;
this.version = version;
this.processTransactions = this.processTransactions.bind(this);
}
processTransactions() {
queueTask(() => {
const running = this.transactions.filter(transaction => transaction._started && transaction._state !== "finished");
const waiting = this.transactions.filter(transaction => !transaction._started && transaction._state !== "finished");
// The next transaction to run is the first waiting one that doesn't overlap with either a running one or a
// preceding waiting one. This allows non-overlapping transactions to run in parallel.
// The exception is readonly transactions, which are allowed to run in parallel with other readonly
// transactions, even with overlapping scopes, since no data is being modified.
const next = waiting.find((transaction, i) => {
const anyRunning = running.some(other => !(transaction.mode === "readonly" && other.mode === "readonly") && intersection(other._scope, transaction._scope).size > 0);
if (anyRunning) {
return false;
}
// If any _preceding_ waiting transactions are blocked, then that's also blocking.
// E.g. if you have 3 transactions: [a], [a,b], and [b,c], then [a] blocks [a,b] which blocks [b,c]
// until [a] is complete, even though [a] and [b,c] share no overlap.
// Note that readonly transactions do not have to be handled as a special case here,
// because if any transactions with overlapping scopes are blocked, then we can assume they are
const anyWaiting = waiting.slice(0, i).some(other => intersection(other._scope, transaction._scope).size > 0);
return !anyWaiting;
});
if (next) {
next.addEventListener("complete", this.processTransactions);
next.addEventListener("abort", this.processTransactions);
next._start();
}
});
}
}// WebIDL requires passing the right number of non-optional arguments, e.g. IDBFactory.open() must have at least 1 arg
function validateRequiredArguments(numArguments, expectedNumArguments, methodName) {
if (numArguments < expectedNumArguments) {
// imitate Firefox's error message
throw new TypeError(`${methodName}: At least ${expectedNumArguments} ${expectedNumArguments === 1 ? "argument" : "arguments"} ` + `required, but only ${arguments.length} passed`);
}
}// https://w3c.github.io/IndexedDB/#connection-queue
const runTaskInConnectionQueue = (connectionQueues, name, task) => {
// Let queue be the connection queue for storageKey and name.
// (note FakeIndexedDB does not support storageKeys currently)
// Add request to queue.
// Wait until all previous requests in queue have been processed.
const queue = connectionQueues.get(name) ?? Promise.resolve();
connectionQueues.set(name, queue.then(task));
};
const waitForOthersClosedDelete = (databases, name, openDatabases, cb) => {
const anyOpen = openDatabases.some(openDatabase2 => {
return !openDatabase2._closed && !openDatabase2._closePending;
});
if (anyOpen) {
queueTask(() => waitForOthersClosedDelete(databases, name, openDatabases, cb));
return;
}
databases.delete(name);
cb(null);
};
// https://w3c.github.io/IndexedDB/#delete-a-database
const deleteDatabase = (databases, connectionQueues, name, request, cb) => {
const deleteDBTask = () => {
return new Promise(resolve => {
const db = databases.get(name);
const oldVersion = db !== undefined ? db.version : 0;
const onComplete = err => {
try {
if (err) {
cb(err);
} else {
cb(null, oldVersion);
}
} finally {
resolve();
}
};
try {
const db = databases.get(name);
if (db === undefined) {
onComplete(null);
return;
}
// Let openConnections be the set of all connections associated with db.
const openConnections = db.connections.filter(connection => {
return !connection._closed;
});
// For each entry of openConnections that does not have its close pending flag set to true, queue a
// database task to fire a version change event named versionchange at entry with db’s version and null.
for (const openDatabase2 of openConnections) {
if (!openDatabase2._closePending) {
queueTask(() => {
const event = new FDBVersionChangeEvent("versionchange", {
newVersion: null,
oldVersion: db.version
});
openDatabase2.dispatchEvent(event);
});
}
}
// Wait for all of the events to be fired. (i.e. queue a task)
queueTask(() => {
// If any of the connections in openConnections are still not closed, queue a database task to
// fire a version change event named blocked at request with db’s version and null.
const anyOpen = openConnections.some(openDatabase3 => {
return !openDatabase3._closed && !openDatabase3._closePending;
});
// If any of the connections in openConnections are still not closed, queue a database task to
// fire a version change event named blocked at request with db’s version and null.
if (anyOpen) {
queueTask(() => {
const event = new FDBVersionChangeEvent("blocked", {
newVersion: null,
oldVersion: db.version
});
request.dispatchEvent(event);
});
}
// Wait until all connections in openConnections are closed.
waitForOthersClosedDelete(databases, name, openConnections, onComplete);
});
} catch (err) {
onComplete(err);
}
});
};
runTaskInConnectionQueue(connectionQueues, name, deleteDBTask);
};
// https://w3c.github.io/IndexedDB/#ref-for-database-version%E2%91%A0%E2%91%A2
const runVersionchangeTransaction = (connection, version, request, cb) => {
connection._runningVersionchangeTransaction = true;
const oldVersion = connection._oldVersion = connection.version;
// Let openConnections be the set of all connections, except connection, associated with db.
const openConnections = connection._rawDatabase.connections.filter(otherDatabase => {
return connection !== otherDatabase;
});
// For each entry of openConnections that does not have its close pending flag set to true, queue a
// database task to fire a version change event named versionchange at entry with db’s version and version.
for (const openDatabase2 of openConnections) {
if (!openDatabase2._closed && !openDatabase2._closePending) {
queueTask(() => {
const event = new FDBVersionChangeEvent("versionchange", {
newVersion: version,
oldVersion
});
openDatabase2.dispatchEvent(event);
});
}
}
// Wait for all of the events to be fired.
// (i.e. queue a task)
queueTask(() => {
const anyOpen = openConnections.some(openDatabase3 => {
return !openDatabase3._closed && !openDatabase3._closePending;
});
// If any of the connections in openConnections are still not closed, queue a database task to fire a version change event named blocked at request with db’s version and version.
if (anyOpen) {
queueTask(() => {
const event = new FDBVersionChangeEvent("blocked", {
newVersion: version,
oldVersion
});
request.dispatchEvent(event);
});
}
// Wait until all connections in openConnections are closed.
const waitForOthersClosed = () => {
const anyOpen2 = openConnections.some(openDatabase2 => {
return !openDatabase2._closed && !openDatabase2._closePending;
});
if (anyOpen2) {
queueTask(waitForOthersClosed);
return;
}
// Set the version of database to version. This change is considered part of the transaction, and so if the
// transaction is aborted, this change is reverted.
connection._rawDatabase.version = version;
connection.version = version;
// Get rid of this setImmediate?
const transaction = connection.transaction(Array.from(connection.objectStoreNames), "versionchange");
// associate the transaction with the open request for later lookup
transaction._openRequest = request;
// https://w3c.github.io/IndexedDB/#upgrade-a-database
// Set request’s result to connection.
request.result = connection;
// Set request’s done flag to true.
request.readyState = "done";
// Set request’s transaction to transaction.
request.transaction = transaction;
transaction._rollbackLog.push(() => {
connection._rawDatabase.version = oldVersion;
connection.version = oldVersion;
});
// Set transaction’s state to active.
transaction._state = "active";
// Let didThrow be the result of firing a version change event named upgradeneeded at request with old version and version.
const event = new FDBVersionChangeEvent("upgradeneeded", {
newVersion: version,
oldVersion
});
let didThrow = false;
try {
request.dispatchEvent(event);
} catch (_err) {
didThrow = true;
}
const concludeUpgrade = () => {
// If transaction’s state is active, then:
if (transaction._state === "active") {
// Set transaction’s state to inactive.
transaction._state = "inactive";
if (didThrow) {
// If didThrow is true, run abort a transaction with transaction and a newly created "AbortError" DOMException.
transaction._abort("AbortError");
}
}
};
// The "upgrade a database" steps are supposed to run as a database task on the database access task source
// (i.e. off the main thread), but since we're actually running on the main thread, we have to be tricky:
// 1. If any `upgradeneeded` event handlers errored, abort synchronously
// 2. Else yield to allow any microtasks to run in response to that event
if (didThrow) {
concludeUpgrade();
} else {
queueTask(concludeUpgrade);
}
transaction._prioritizedListeners.set("error", () => {
connection._runningVersionchangeTransaction = false;
connection._oldVersion = undefined;
// throw arguments[0].target.error;
// console.log("error in versionchange transaction - not sure if anything needs to be done here", e.target.error.name);
});
transaction._prioritizedListeners.set("abort", () => {
connection._runningVersionchangeTransaction = false;
connection._oldVersion = undefined;
queueTask(() => {
// Reset transaction in a tick after onabort (upgrade-transaction-lifecycle-user-aborted.any)
request.transaction = null;
cb(new AbortError());
});
});
transaction._prioritizedListeners.set("complete", () => {
connection._runningVersionchangeTransaction = false;
connection._oldVersion = undefined;
// Let other complete event handlers run before continuing
queueTask(() => {
// Reset transaction in a tick after oncomplete (upgrade-transaction-lifecycle-committed.any.js)
request.transaction = null;
if (connection._closePending) {
cb(new AbortError());
} else {
cb(null);
}
});
});
};
waitForOthersClosed();
});
};
// https://w3c.github.io/IndexedDB/#opening
const openDatabase = (databases, connectionQueues, name, version, request, cb) => {
const openDBTask = () => {
return new Promise(resolve => {
const onComplete = err => {
try {
if (err) {
// DO THIS HERE: ensure that connection is closed by running the steps for closing a database connection before these
// steps are aborted.
cb(err);
} else {
cb(null, connection);
}
} finally {
resolve();
}
};
// Let db be the database named name in storageKey, or null otherwise.
let db = databases.get(name);
if (db === undefined) {
// If db is null, let db be a new database with name `name`, version 0 (zero), and with no object stores.
db = new Database(name, 0);
databases.set(name, db);
}
// If version is undefined, let version be 1 if db is null, or db’s version otherwise.
if (version === undefined) {
version = db.version !== 0 ? db.version : 1;
}
// If db’s version is greater than version, return a newly created "VersionError" DOMException and abort these steps.
if (db.version > version) {
return onComplete(new VersionError());
}
// Let connection be a new connection to db.
const connection = new FDBDatabase(db);
// If db’s version is less than version, then:
if (db.version < version) {
// (run a version change transaction and resolve so that the next promise in the queue will execute)
runVersionchangeTransaction(connection, version, request, err => {
onComplete(err);
});
} else {
onComplete(null);
}
});
};
runTaskInConnectionQueue(connectionQueues, name, openDBTask);
};
class FDBFactory {
_databases = new Map();
// https://w3c.github.io/IndexedDB/#connection-queue
_connectionQueues = new Map(); // promise chain as lightweight FIFO task queue
// https://w3c.github.io/IndexedDB/#dom-idbfactory-cmp
cmp(first, second) {
validateRequiredArguments(arguments.length, 2, "IDBFactory.cmp");
return cmp(first, second);
}
// https://w3c.github.io/IndexedDB/#dom-idbfactory-deletedatabase
deleteDatabase(name) {
validateRequiredArguments(arguments.length, 1, "IDBFactory.deleteDatabase");
const request = new FDBOpenDBRequest();
request.source = null;
queueTask(() => {
deleteDatabase(this._databases, this._connectionQueues, name, request, (err, oldVersion) => {
if (err) {
request.error = new DOMException(err.message, err.name);
request.readyState = "done";
const event = new Event("error", {
bubbles: true,
cancelable: true
});
event.eventPath = [];
request.dispatchEvent(event);
return;
}
request.result = undefined;
request.readyState = "done";
const event2 = new FDBVersionChangeEvent("success", {
newVersion: null,
oldVersion
});
request.dispatchEvent(event2);
});
});
return request;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBFactory-open-IDBOpenDBRequest-DOMString-name-unsigned-long-long-version
open(name, version) {
validateRequiredArguments(arguments.length, 1, "IDBFactory.open");
if (arguments.length > 1 && version !== undefined) {
// Based on spec, not sure why "MAX_SAFE_INTEGER" instead of "unsigned long long", but it's needed to pass
// tests
version = enforceRange(version, "MAX_SAFE_INTEGER");
}
if (version === 0) {
throw new TypeError("Database version cannot be 0");
}
const request = new FDBOpenDBRequest();
request.source = null;
queueTask(() => {
openDatabase(this._databases, this._connectionQueues, name, version, request, (err, connection) => {
if (err) {
request.result = undefined;
request.readyState = "done";
request.error = new DOMException(err.message, err.name);
const event = new Event("error", {
bubbles: true,
cancelable: true
});
event.eventPath = [];
request.dispatchEvent(event);
return;
}
request.result = connection;
request.readyState = "done";
const event2 = new Event("success");
event2.eventPath = [];
request.dispatchEvent(event2);
});
});
return request;
}
// https://w3c.github.io/IndexedDB/#dom-idbfactory-databases
databases() {
return Promise.resolve(Array.from(this._databases.entries(), ([name, database]) => {
const activeVersionChangeConnection = database.connections.find(connection => connection._runningVersionchangeTransaction);
// If a versionchange is in progress, report the old version. See `get-databases.any.js` test:
// "The result of databases() should contain the versions of databases at the time of calling,
// regardless of versionchange transactions currently running."
const version = activeVersionChangeConnection ? activeVersionChangeConnection._oldVersion : database.version;
return {
name,
version
};
}).filter(({
version
}) => {
// Ignore newly-created DBs with active versionchange transactions. See `get-databases.any.js` test:
// "The result of databases() should be only those databases which have been created at the
// time of calling, regardless of versionchange transactions currently running."
return version > 0;
}));
}
get [Symbol.toStringTag]() {
return "IDBFactory";
}
}const fakeIndexedDB = new FDBFactory();// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type -- any function
function createFakeNativeFunction(cb) {
const fnName = cb.name || '';
const toStringFn = ()=>`function ${fnName}() { [native code] }`;
Object.defineProperty(cb, 'toString', {
value: toStringFn,
writable: true,
configurable: false,
enumerable: false
});
Object.defineProperty(cb, 'toLocaleString', {
value: toStringFn,
writable: true,
configurable: false,
enumerable: false
});
return cb;
}const defusedPattern = o$1([
'pbp3',
'pbp_',
'pbpstate',
'BILI_MIRROR',
'MIRROR_TRACK',
'__LOG',
'reporter-pb',
'KV_CONFIG_SDK',
'pcdn',
'nc_loader',
'bpcfgzip'
]);
const defuseStorage = {
name: 'disable-storage',
description: '防止叔叔浪费你宝贵的 SSD 寿命',
any () {
deleteIndexedDB();
((origOpen)=>{
unsafeWindow.indexedDB.open = createFakeNativeFunction(function(name, version) {
if (defusedPattern(name)) {
logger.trace('IndexedDB mocked!', {
name,
version
});
return fakeIndexedDB.open(name, version);
}
logger.trace('IndexedDB opened!', {
name,
version
});
return origOpen.call(this, name, version);
});
// eslint-disable-next-line @typescript-eslint/unbound-method -- override native method
})(unsafeWindow.indexedDB.open);
((orignalLocalStorage)=>{
for(let i = 0; i < orignalLocalStorage.length; i++){
const key = orignalLocalStorage.key(i);
if (key && defusedPattern(key)) {
orignalLocalStorage.removeItem(key);
logger.info('localStorage removed!', {
key
});
}
}
const store = new Map();
const keys = Object.keys(orignalLocalStorage);
const mockedLocalStorage = {
setItem (key, value) {
keys.push(key);
if (defusedPattern(key)) {
logger.trace('localStorage.setItem mocked:', {
key,
value
});
orignalLocalStorage.removeItem(key);
store.set(key, value);
} else orignalLocalStorage.setItem(key, value);
},
getItem (key) {
if (defusedPattern(key)) {
const value = store.has(key) ? store.get(key) : null;
logger.trace('localStorage.getItem mocked:', {
key,
value
});
return value;
}
// logger.trace('localStorage.getItem:', { key });
return orignalLocalStorage.getItem(key);
},
removeItem (key) {
const keyIndex = keys.indexOf(key);
if (keyIndex > -1) keys.splice(keys.indexOf(key), 1);
if (defusedPattern(key)) {
logger.trace('localStorage.removeItem mocked:', {
key
});
store.delete(key);
} else // logger.trace('localStorage.removeItem:', { key });
orignalLocalStorage.removeItem(key);
},
clear () {
logger.trace('localStorage.clear mocked');
store.clear();
orignalLocalStorage.clear();
keys.length = 0;
},
get length () {
return store.size + localStorage.length;
},
key (index) {
const realIndex = keys.length - index - 1;
return keys[realIndex] ?? null;
}
};
Object.defineProperty(unsafeWindow, 'localStorage', {
get () {
return mockedLocalStorage;
},
enumerable: true,
configurable: false,
set: o$2
});
})(unsafeWindow.localStorage);
}
};
async function deleteIndexedDB() {
if (!('databases' in unsafeWindow.indexedDB)) return;
const dbs = await unsafeWindow.indexedDB.databases();
for(let i = 0, len = dbs.length; i < len; i++){
const db = dbs[i];
if (db.name && defusedPattern(db.name)) {
logger.info('IndexedDB deleted!', {
name: db.name
});
unsafeWindow.indexedDB.deleteDatabase(db.name);
}
}
}function hook({ onlyCallOnce }) {
if (localStorage.getItem('bilibili_player_force_DolbyAtmos&8K&HDR') !== '1') localStorage.setItem('bilibili_player_force_DolbyAtmos&8K&HDR', '1');
if (localStorage.getItem('bilibili_player_force_hdr') !== '1') localStorage.setItem('bilibili_player_force_hdr', '1');
((sessionStorageGetItem)=>{
sessionStorage.getItem = function(key) {
// 部分視頻解碼錯誤後會強制全局回退,禁用所有HEVC內容
// 此hook禁用對應邏輯
if (key === 'enableHEVCError') return null;
return Reflect.apply(sessionStorageGetItem, this, [
key
]);
};
// eslint-disable-next-line @typescript-eslint/unbound-method -- cache origin method
})(sessionStorage.getItem);
onlyCallOnce(overrideUA);
}
function overrideUA() {
// Bilibili use User-Agent to determine if the 4K should be avaliable, we simply overrides UA
defineReadonlyProperty(unsafeWindow.navigator, 'userAgent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.3 Safari/605.1.15');
}
const forceEnable4K = {
name: 'force-enable-4k',
description: '强制启用 4K 播放',
onVideo: hook,
onBangumi: hook,
onLive: hook
};const KEY_PREFIX = 'mbgtbemodule:';
function initModuleMenu(mod) {
const enabled = getEnabled(mod);
GM.registerMenuCommand(labelFor(enabled, mod), async ()=>{
const current = getEnabled(mod);
await setEnabled(mod, !current);
try {
unsafeWindow.location.reload();
} catch {
// swallow
}
});
return enabled;
}
function getEnabled(m) {
return GM_getValue(KEY_PREFIX + m.name, true);
}
async function setEnabled(m, enabled) {
return GM.setValue(KEY_PREFIX + m.name, enabled);
}
function labelFor(enabled, m) {
// use ASCII-friendly symbols to avoid linter/encoding issues
const mark = enabled ? '[ON]' : '[OFF]';
return `${mark} ${m.description}`;
}((unsafeWindow1)=>{
const modules = [
defuseStorage,
defuseSpyware,
disableAV1,
enhanceLive,
fixCopyInCV,
forceEnable4K,
noAd,
noP2P,
noWebRTC,
optimizeHomepage,
optimizeStory,
playerVideoFit,
removeBlackBackdropFilter,
removeUselessUrlParams,
useSystemFonts
];
const styles = [];
const onBeforeFetchHooks = new Set();
const onResponseHooks = new Set();
const onXhrOpenHooks = new Set();
const onAfterXhrOpenHooks = new Set();
const onXhrResponseHooks = new Set();
const fnWs = new WeakSet();
function onlyCallOnce(fn) {
if (fnWs.has(fn)) return;
fnWs.add(fn);
fn();
}
const hook = {
addStyle (style) {
styles.push(style);
},
onBeforeFetch (cb) {
onBeforeFetchHooks.add(cb);
},
onResponse (cb) {
onResponseHooks.add(cb);
},
onXhrOpen (cb) {
onXhrOpenHooks.add(cb);
},
onAfterXhrOpen (cb) {
onAfterXhrOpenHooks.add(cb);
},
onXhrResponse (cb) {
onXhrResponseHooks.add(cb);
},
onlyCallOnce
};
const hostname = unsafeWindow1.location.hostname;
const pathname = unsafeWindow1.location.pathname;
for(let i = 0, len = modules.length; i < len; i++){
const mod = modules[i];
const enabled = initModuleMenu(mod);
if (!enabled) {
logger.log(`[${mod.name}] disabled -- skipping`);
continue;
}
if (mod.any) {
logger.log(`[${mod.name}] "any" ${unsafeWindow1.location.href}`);
mod.any(hook);
}
switch(hostname){
case 'www.bilibili.com':
if (pathname.startsWith('/read/cv')) {
if (mod.onCV) {
logger.log(`[${mod.name}] "onCV" ${unsafeWindow1.location.href}`);
mod.onCV(hook);
}
} else if (pathname.startsWith('/video/')) {
if (mod.onVideo) {
logger.log(`[${mod.name}] "onVideo" ${unsafeWindow1.location.href}`);
mod.onVideo(hook);
}
if (mod.onVideoOrBangumi) {
logger.log(`[${mod.name}] "onVideoOrBangumi" ${unsafeWindow1.location.href}`);
mod.onVideoOrBangumi(hook);
}
} else if (pathname.startsWith('/bangumi/play/')) {
if (mod.onVideo) {
logger.log(`[${mod.name}] "onVideo" ${unsafeWindow1.location.href}`);
mod.onVideo(hook);
}
if (mod.onBangumi) {
logger.log(`[${mod.name}] "onBangumi" ${unsafeWindow1.location.href}`);
mod.onBangumi(hook);
}
if (mod.onVideoOrBangumi) {
logger.log(`[${mod.name}] "onVideoOrBangumi" ${unsafeWindow1.location.href}`);
mod.onVideoOrBangumi(hook);
}
}
break;
case 'live.bilibili.com':
if (mod.onLive) {
logger.log(`[${mod.name}] "onLive" ${unsafeWindow1.location.href}`);
mod.onLive(hook);
}
break;
case 't.bilibili.com':
if (mod.onStory) {
logger.log(`[${mod.name}] "onStory" ${unsafeWindow1.location.href}`);
mod.onStory(hook);
}
break;
}
}
// Add Style
const sheet = new CSSStyleSheet();
sheet.replaceSync(styles.join('\n'));
document.adoptedStyleSheets.push(sheet);
// Override fetch
(($fetch)=>{
unsafeWindow1.fetch = async function(...$fetchArgs) {
let abortFetch = false;
// eslint-disable-next-line no-useless-assignment -- the assignment can be skipped if doBeforeFetch throws an error
let fetchArgs = $fetchArgs;
let mockResponse = null;
for (const onBeforeFetch of onBeforeFetchHooks)try {
fetchArgs = onBeforeFetch($fetchArgs);
if (fetchArgs === null) {
abortFetch = true;
break;
} else if ('body' in fetchArgs) {
abortFetch = true;
mockResponse = fetchArgs;
break;
}
} catch (e) {
logger.error('Failed to replace fetcherArgs', e, {
fetchArgs: $fetchArgs
});
}
if (abortFetch) {
logger.debug('Fetch aborted', {
fetchArgs: $fetchArgs,
mockResponse
});
return mockResponse ?? new Response();
}
let response = await Reflect.apply($fetch, this, $fetchArgs);
for (const onResponse of onResponseHooks)// eslint-disable-next-line no-await-in-loop -- hook
response = await onResponse(response, $fetchArgs, $fetch);
return response;
};
})(unsafeWindow1.fetch);
const xhrInstances = new WeakMap();
const XHRBefore = unsafeWindow1.XMLHttpRequest.prototype;
unsafeWindow1.XMLHttpRequest = class extends unsafeWindow1.XMLHttpRequest {
open(...$args) {
const method = $args[0];
const url = $args[1];
const xhrDetails = {
method,
url,
response: null,
lastResponseLength: null
};
let xhrArgs = $args;
for (const onXhrOpen of onXhrOpenHooks)try {
if (xhrArgs === null) break;
xhrArgs = onXhrOpen(xhrArgs, this);
} catch (e) {
logger.error('Failed to replace P2P for XMLHttpRequest.prototype.open', e);
}
if (xhrArgs === null) {
logger.debug('XHR aborted', {
$args
});
this.send = o$2;
this.setRequestHeader = o$2;
return;
}
xhrInstances.set(this, xhrDetails);
super.open(...xhrArgs);
for (const onAfterXhrOpen of onAfterXhrOpenHooks)try {
onAfterXhrOpen(this);
} catch (e) {
logger.error('Failed to call onAfterXhrOpen', e);
}
}
get response() {
const originalResponse = super.response;
if (!xhrInstances.has(this)) return originalResponse;
const xhrDetails = xhrInstances.get(this);
const responseLength = typeof originalResponse === 'string' ? originalResponse.length : null;
if (xhrDetails.lastResponseLength !== responseLength) {
xhrDetails.response = null;
xhrDetails.lastResponseLength = responseLength;
}
if (xhrDetails.response !== null) return xhrDetails.response;
let finalResponse = originalResponse;
for (const onXhrResponse of onXhrResponseHooks)try {
finalResponse = onXhrResponse(xhrDetails.method, xhrDetails.url, finalResponse, this);
} catch (e) {
logger.error('Failed to call onXhrResponse', e);
}
xhrDetails.response = finalResponse;
return finalResponse;
}
get responseText() {
const response = this.response;
return typeof response === 'string' ? response : super.responseText;
}
};
unsafeWindow1.XMLHttpRequest.prototype.open.toString = function() {
return XHRBefore.open.toString();
};
unsafeWindow1.XMLHttpRequest.prototype.send.toString = function() {
return XHRBefore.send.toString();
};
// unsafeWindow.XMLHttpRequest.prototype.getResponseHeader.toString = function () {
// return XHRBefore.getResponseHeader.toString();
// };
// unsafeWindow.XMLHttpRequest.prototype.getAllResponseHeaders.toString = function () {
// return XHRBefore.getAllResponseHeaders.toString();
// };
})(unsafeWindow);})();