add firefox private mode suppport;add fake-indexeddb;

This commit is contained in:
suziwen 2024-08-13 12:54:38 +08:00
parent 71e7bed82b
commit fdfa1a25c3
39 changed files with 3540 additions and 478 deletions

View File

@ -0,0 +1,67 @@
import fakeIndexedDB from "./lib/fake-indexeddb/fakeIndexedDB.js";
import FDBCursor from "./lib/fake-indexeddb/FDBCursor.js";
import FDBCursorWithValue from "./lib/fake-indexeddb/FDBCursorWithValue.js";
import FDBDatabase from "./lib/fake-indexeddb/FDBDatabase.js";
import FDBFactory from "./lib/fake-indexeddb/FDBFactory.js";
import FDBIndex from "./lib/fake-indexeddb/FDBIndex.js";
import FDBKeyRange from "./lib/fake-indexeddb/FDBKeyRange.js";
import FDBObjectStore from "./lib/fake-indexeddb/FDBObjectStore.js";
import FDBOpenDBRequest from "./lib/fake-indexeddb/FDBOpenDBRequest.js";
import FDBRequest from "./lib/fake-indexeddb/FDBRequest.js";
import FDBTransaction from "./lib/fake-indexeddb/FDBTransaction.js";
import FDBVersionChangeEvent from "./lib/fake-indexeddb/FDBVersionChangeEvent.js";
/**
* author: suziwen1@gmail.com
**/
const ZeroIndexedDBFactory = ()=> {
var globalVar =
typeof window !== "undefined"
? window
: typeof WorkerGlobalScope !== "undefined"
? self
: typeof global !== "undefined"
? global
: Function("return this;")();
Object.defineProperty(globalVar, 'indexedDB', {
value: fakeIndexedDB
});
Object.defineProperty(globalVar, 'IDBCursor', {
value: FDBCursor
});
Object.defineProperty(globalVar, 'IDBCursorWithValue', {
value: FDBCursorWithValue
});
Object.defineProperty(globalVar, 'IDBDatabase', {
value: FDBDatabase
});
Object.defineProperty(globalVar, 'IDBFactory', {
value: FDBFactory
});
Object.defineProperty(globalVar, 'IDBIndex', {
value: FDBIndex
});
Object.defineProperty(globalVar, 'IDBKeyRange', {
value: FDBKeyRange
});
Object.defineProperty(globalVar, 'IDBObjectStore', {
value: FDBObjectStore
});
Object.defineProperty(globalVar, 'IDBOpenDBRequest', {
value: FDBOpenDBRequest
});
Object.defineProperty(globalVar, 'IDBRequest', {
value: FDBRequest
});
Object.defineProperty(globalVar, 'IDBTransaction', {
value: FDBTransaction
});
Object.defineProperty(globalVar, 'IDBVersionChangeEvent', {
value: FDBVersionChangeEvent
});
}
export default ZeroIndexedDBFactory

View File

@ -1,4 +1,7 @@
'use strict'
/**
* author: suziwen1@gmail.com
**/
let valuesMap = new Map()
class LocalStorage {
@ -42,7 +45,7 @@ class LocalStorage {
}
const instance = new LocalStorage()
globalThis.zeroLocalStorage = new Proxy(instance, {
const zeroLocalStorage = new Proxy(instance, {
set: function (obj, prop, value) {
if (LocalStorage.prototype.hasOwnProperty(prop)) {
instance[prop] = value
@ -60,6 +63,5 @@ globalThis.zeroLocalStorage = new Proxy(instance, {
}
}
})
if (!globalThis.localStorage) {
globalThis.localStorage = globalThis.zeroLocalStorage;
}
export default zeroLocalStorage

View File

@ -1,115 +1,119 @@
const logStore = idbKeyval.createStore('log-store', 'log-store');
const ZeroLogFactory = ()=>{
const logStore = idbKeyval.createStore('log-store', 'log-store');
const logSequence = []
let isRunning = false
let splitStr = '\n----Z-e-r-o-O-m-e-g-a--------------\n'
const logSequence = []
let isRunning = false
let splitStr = '\n----Z-e-r-o-O-m-e-g-a--------------\n'
const originConsoleLog = console.log
const originConsoleError = console.error
const originConsoleLog = console.log
const originConsoleError = console.error
const _logFn = async function(){
if (isRunning) return
isRunning = true
const _moment = moment()
const _logFn = async function(){
if (isRunning) return
isRunning = true
const _moment = moment()
const dayOfWeek = _moment.format('E') // Day of Week (ISO), keep logs max 7 day
const monthNum = _moment.format('DD')
const logKey = 'zerolog-' + dayOfWeek
while (logSequence.length > 0) {
const str = logSequence.join('\n');
logSequence.length = 0;
let logInfo = await idbKeyval.get(logKey, logStore)
let date = _moment.format('YYYY-MM-DD')
if (!logInfo || !logInfo.date) {
logInfo = { date: date, val: ''}
}
let { val } = logInfo
if ( logInfo.date != date) {
val = ''
}
val += splitStr
splitStr = `\n`
val += str
await idbKeyval.set(logKey, { date, val }, logStore)
}
isRunning = false
}
const logFn = (str)=>{
logSequence.push(moment().format('YYYY-MM-DD HH:mm:ss ') + ` ` + str)
_logFn()
}
const replacerFn = (key, value)=>{
switch (key) {
case 'username':
case 'password':
case 'host':
case 'port':
case 'token':
case 'gistToken':
case 'gistId':
return '<secret>'
default:
return value
}
}
const getStr = function (){
const strArgs = [...arguments].map((obj)=>{
let str = '';
try {
if (typeof obj == 'string') {
str = obj
} else {
str = JSON.stringify(obj, replacerFn, 4)
const dayOfWeek = _moment.format('E') // Day of Week (ISO), keep logs max 7 day
const monthNum = _moment.format('DD')
const logKey = 'zerolog-' + dayOfWeek
while (logSequence.length > 0) {
const str = logSequence.join('\n');
logSequence.length = 0;
let logInfo = await idbKeyval.get(logKey, logStore)
let date = _moment.format('YYYY-MM-DD')
if (!logInfo || !logInfo.date) {
logInfo = { date: date, val: ''}
}
} catch(e){
let { val } = logInfo
if ( logInfo.date != date) {
val = ''
}
val += splitStr
splitStr = `\n`
val += str
await idbKeyval.set(logKey, { date, val }, logStore)
}
isRunning = false
}
const logFn = (str)=>{
logSequence.push(moment().format('YYYY-MM-DD HH:mm:ss ') + ` ` + str)
_logFn()
}
const replacerFn = (key, value)=>{
switch (key) {
case 'username':
case 'password':
case 'host':
case 'port':
case 'token':
case 'gistToken':
case 'gistId':
return '<secret>'
default:
return value
}
}
const getStr = function (){
const strArgs = [...arguments].map((obj)=>{
let str = '';
try {
str = obj.toString()
if (typeof obj == 'string') {
str = obj
} else {
str = JSON.stringify(obj, replacerFn, 4)
}
} catch(e){
try {
str = obj.toString()
} catch(e){
}
}
}
return str
})
return strArgs.join(' ')
}
const ZeroLog = function(){
logFn(getStr.apply(null, arguments))
}
const _lastErrorLogFn = async ()=>{
if (_lastErrorLogFn.isRunning) return
_lastErrorLogFn.isRunning = true
while (_lastErrorLogFn.val) {
const val = _lastErrorLogFn.val
_lastErrorLogFn.val = ''
await idbKeyval.set('lastError', val, logStore)
return str
})
return strArgs.join(' ')
}
_lastErrorLogFn.isRunning = false
const ZeroLog = function(){
logFn(getStr.apply(null, arguments))
}
const _lastErrorLogFn = async ()=>{
if (_lastErrorLogFn.isRunning) return
_lastErrorLogFn.isRunning = true
while (_lastErrorLogFn.val) {
const val = _lastErrorLogFn.val
_lastErrorLogFn.val = ''
await idbKeyval.set('lastError', val, logStore)
}
_lastErrorLogFn.isRunning = false
}
const lastErrorLogFn = async function (){
const val = getStr.apply(null, arguments)
_lastErrorLogFn.val = val
_lastErrorLogFn()
}
const ZeroLogInfo = function() {
originConsoleLog.apply(null, arguments)
ZeroLog.apply(null, ['[INFO]', ...arguments])
}
const ZeroLogError = function(){
originConsoleError.apply(null, arguments)
ZeroLog.apply(null, ['[ERROR]', ...arguments])
lastErrorLogFn.apply(null, arguments)
}
const ZeroLogClear = async function(){
await idbKeyval.clear(logStore)
}
console.log = ZeroLogInfo
console.error = ZeroLogError
}
const lastErrorLogFn = async function (){
const val = getStr.apply(null, arguments)
_lastErrorLogFn.val = val
_lastErrorLogFn()
}
globalThis.ZeroLogInfo = function() {
originConsoleLog.apply(null, arguments)
ZeroLog.apply(null, ['[INFO]', ...arguments])
}
globalThis.ZeroLogError = function(){
originConsoleError.apply(null, arguments)
ZeroLog.apply(null, ['[ERROR]', ...arguments])
lastErrorLogFn.apply(null, arguments)
}
globalThis.ZeroLogClear = async function(){
await idbKeyval.clear(logStore)
}
console.log = ZeroLogInfo
console.error = ZeroLogError
export default ZeroLogFactory

View File

@ -1,7 +1,10 @@
import zeroLocalStorage from "./localstorage-polyfill.js"
import ZeroLogFactory from './log.js'
import ZeroIndexedDBFactory from './indexedDB.js'
import "./js/background_preload.js"
import "./lib/idb-keyval.js"
import "./lib/moment-with-locales.js"
import "./localstorage-polyfill.js"
import "./lib/csso.js"
import "./js/log_error.js"
import "./log.js"
@ -11,4 +14,33 @@ import "./js/omega_pac.min.js"
import "./js/omega_target.min.js"
import "./js/omega_target_chromium_extension.min.js"
import "./img/icons/draw_omega.js"
import "./js/background.js"
import "./js/background.js" // zeroBackground
/**
* author: suziwen1@gmail.com
**/
const isFirefox = !!globalThis.localStorage
function detectPrivateMode(cb) {
var db,
on = cb.bind(null, true),
off = cb.bind(null, false)
if (isFirefox) {
db = indexedDB.open("zeroOmega-test"), db.onerror = on, db.onsuccess = off
} else {
off()
}
}
detectPrivateMode(function (isPrivateMode) {
if (isPrivateMode && isFirefox) {
// fake indexedDB
ZeroIndexedDBFactory()
}
ZeroLogFactory()
const zeroStorage = isFirefox ? localStorage : zeroLocalStorage
globalThis.zeroBackground(zeroStorage)
console.log('is private mode: ' + isPrivateMode)
})

View File

@ -1,374 +1,380 @@
OmegaTargetCurrent = Object.create(OmegaTargetChromium)
Promise = OmegaTargetCurrent.Promise
Promise.longStackTraces()
zeroBackground = (zeroStorage, opts) ->
OmegaTargetCurrent = Object.create(OmegaTargetChromium)
Promise = OmegaTargetCurrent.Promise
Promise.longStackTraces()
OmegaTargetCurrent.Log = Object.create(OmegaTargetCurrent.Log)
Log = OmegaTargetCurrent.Log
OmegaTargetCurrent.Log = Object.create(OmegaTargetCurrent.Log)
Log = OmegaTargetCurrent.Log
# TODO (suziwen1@gmail.com)
globalThis.isBrowserRestart = globalThis.startupCheck is undefined
startupCheck = globalThis.startupCheck ?= -> true
globalThis.isBrowserRestart = globalThis.startupCheck is undefined
startupCheck = globalThis.startupCheck ?= -> true
chrome.runtime.onStartup.addListener ->
globalThis.isBrowserRestart = true
chrome.runtime.onStartup.addListener ->
globalThis.isBrowserRestart = true
unhandledPromises = []
unhandledPromisesId = []
unhandledPromisesNextId = 1
Promise.onPossiblyUnhandledRejection (reason, promise) ->
Log.error("[#{unhandledPromisesNextId}] Unhandled rejection:\n", reason)
unhandledPromises.push(promise)
unhandledPromisesId.push(unhandledPromisesNextId)
unhandledPromisesNextId++
Promise.onUnhandledRejectionHandled (promise) ->
index = unhandledPromises.indexOf(promise)
Log.log("[#{unhandledPromisesId[index]}] Rejection handled!", promise)
unhandledPromises.splice(index, 1)
unhandledPromisesId.splice(index, 1)
unhandledPromises = []
unhandledPromisesId = []
unhandledPromisesNextId = 1
Promise.onPossiblyUnhandledRejection (reason, promise) ->
Log.error("[#{unhandledPromisesNextId}] Unhandled rejection:\n", reason)
unhandledPromises.push(promise)
unhandledPromisesId.push(unhandledPromisesNextId)
unhandledPromisesNextId++
Promise.onUnhandledRejectionHandled (promise) ->
index = unhandledPromises.indexOf(promise)
Log.log("[#{unhandledPromisesId[index]}] Rejection handled!", promise)
unhandledPromises.splice(index, 1)
unhandledPromisesId.splice(index, 1)
iconCache = {}
drawContext = null
drawError = null
drawIcon = (resultColor, profileColor) ->
cacheKey = "omega+#{resultColor ? ''}+#{profileColor}"
icon = iconCache[cacheKey]
return icon if icon
try
if not drawContext?
canvas = new OffscreenCanvas(300, 300)
drawContext = canvas.getContext('2d', { willReadFrequently: true })
iconCache = {}
drawContext = null
drawError = null
drawIcon = (resultColor, profileColor) ->
cacheKey = "omega+#{resultColor ? ''}+#{profileColor}"
icon = iconCache[cacheKey]
return icon if icon
try
if not drawContext?
canvas = new OffscreenCanvas(300, 300)
drawContext = canvas.getContext('2d', { willReadFrequently: true })
icon = {}
for size in [16, 19, 24, 32, 38]
drawContext.scale(size, size)
drawContext.clearRect(0, 0, 1, 1)
if resultColor?
drawOmega drawContext, resultColor, profileColor
else
drawOmega drawContext, profileColor
drawContext.setTransform(1, 0, 0, 1, 0, 0)
icon[size] = drawContext.getImageData(0, 0, size, size)
if icon[size].data[3] == 255
# Some browsers may replace the image data with a opaque white image to
# resist fingerprinting. In that case the icon cannot be drawn.
throw new Error('Icon drawing blocked by privacy.resistFingerprinting.')
catch e
if not drawError?
drawError = e
Log.error(e)
Log.error('Profile-colored icon disabled. Falling back to static icon.')
icon = null
return iconCache[cacheKey] = icon
charCodeUnderscore = '_'.charCodeAt(0)
isHidden = (name) -> (name.charCodeAt(0) == charCodeUnderscore and
name.charCodeAt(1) == charCodeUnderscore)
dispName = (name) -> chrome.i18n.getMessage('profile_' + name) || name
actionForUrl = (url) ->
options.ready.then(->
request = OmegaPac.Conditions.requestFromUrl(url)
options.matchProfile(request)
).then(({profile, results}) ->
current = options.currentProfile()
currentName = dispName(current.name)
if current.profileType == 'VirtualProfile'
realCurrentName = current.defaultProfileName
currentName += " [#{dispName(realCurrentName)}]"
current = options.profile(realCurrentName)
details = ''
direct = false
attached = false
condition2Str = (condition) ->
condition.pattern || OmegaPac.Conditions.str(condition)
for result in results
if Array.isArray(result)
if not result[1]?
attached = false
name = result[0]
if name[0] == '+'
name = name.substr(1)
if isHidden(name)
attached = true
else if name != realCurrentName
details += chrome.i18n.getMessage 'browserAction_defaultRuleDetails'
details += " => #{dispName(name)}\n"
else if result[1].length == 0
if result[0] == 'DIRECT'
details += chrome.i18n.getMessage('browserAction_directResult')
details += '\n'
direct = true
else
details += "#{result[0]}\n"
else if typeof result[1] == 'string'
details += "#{result[1]} => #{result[0]}\n"
icon = {}
for size in [16, 19, 24, 32, 38]
drawContext.scale(size, size)
drawContext.clearRect(0, 0, 1, 1)
if resultColor?
drawOmega drawContext, resultColor, profileColor
else
condition = condition2Str(result[1].condition ? result[1])
details += "#{condition} => "
if result[0] == 'DIRECT'
details += chrome.i18n.getMessage('browserAction_directResult')
details += '\n'
direct = true
drawOmega drawContext, profileColor
drawContext.setTransform(1, 0, 0, 1, 0, 0)
icon[size] = drawContext.getImageData(0, 0, size, size)
if icon[size].data[3] == 255
# Some browsers may replace the image data
# with a opaque white image to
# resist fingerprinting. In that case the icon cannot be drawn.
throw new Error(
'Icon drawing blocked by privacy.resistFingerprinting.')
catch e
if not drawError?
drawError = e
Log.error(e)
Log.error('Profile-colored icon disabled. Falling back to static icon.')
icon = null
return iconCache[cacheKey] = icon
charCodeUnderscore = '_'.charCodeAt(0)
isHidden = (name) -> (name.charCodeAt(0) == charCodeUnderscore and
name.charCodeAt(1) == charCodeUnderscore)
dispName = (name) -> chrome.i18n.getMessage('profile_' + name) || name
actionForUrl = (url) ->
options.ready.then(->
request = OmegaPac.Conditions.requestFromUrl(url)
options.matchProfile(request)
).then(({profile, results}) ->
current = options.currentProfile()
currentName = dispName(current.name)
if current.profileType == 'VirtualProfile'
realCurrentName = current.defaultProfileName
currentName += " [#{dispName(realCurrentName)}]"
current = options.profile(realCurrentName)
details = ''
direct = false
attached = false
condition2Str = (condition) ->
condition.pattern || OmegaPac.Conditions.str(condition)
for result in results
if Array.isArray(result)
if not result[1]?
attached = false
name = result[0]
if name[0] == '+'
name = name.substr(1)
if isHidden(name)
attached = true
else if name != realCurrentName
details +=
chrome.i18n.getMessage 'browserAction_defaultRuleDetails'
details += " => #{dispName(name)}\n"
else if result[1].length == 0
if result[0] == 'DIRECT'
details += chrome.i18n.getMessage('browserAction_directResult')
details += '\n'
direct = true
else
details += "#{result[0]}\n"
else if typeof result[1] == 'string'
details += "#{result[1]} => #{result[0]}\n"
else
details += "#{result[0]}\n"
else if result.profileName
if result.isTempRule
details += chrome.i18n.getMessage('browserAction_tempRulePrefix')
else if attached
details += chrome.i18n.getMessage('browserAction_attachedPrefix')
attached = false
condition = result.source ? condition2Str(result.condition)
details += "#{condition} => #{dispName(result.profileName)}\n"
condition = condition2Str(result[1].condition ? result[1])
details += "#{condition} => "
if result[0] == 'DIRECT'
details += chrome.i18n.getMessage('browserAction_directResult')
details += '\n'
direct = true
else
details += "#{result[0]}\n"
else if result.profileName
if result.isTempRule
details += chrome.i18n.getMessage('browserAction_tempRulePrefix')
else if attached
details += chrome.i18n.getMessage('browserAction_attachedPrefix')
attached = false
condition = result.source ? condition2Str(result.condition)
details += "#{condition} => #{dispName(result.profileName)}\n"
if not details
details = options.printProfile(current)
if not details
details = options.printProfile(current)
resultColor = profile.color
profileColor = current.color
icon = null
if direct
resultColor = options.profile('direct').color
profileColor = profile.color
else if profile.name == current.name and options.isCurrentProfileStatic()
resultColor = profileColor = profile.color
icon = drawIcon(profile.color)
else
resultColor = profile.color
profileColor = current.color
icon ?= drawIcon(resultColor, profileColor)
icon = null
if direct
resultColor = options.profile('direct').color
profileColor = profile.color
else if profile.name == current.name and options.isCurrentProfileStatic()
resultColor = profileColor = profile.color
icon = drawIcon(profile.color)
else
resultColor = profile.color
profileColor = current.color
shortTitle = 'Omega: ' + currentName # TODO: I18n.
if profile.name != currentName
shortTitle += ' => ' + profile.name # TODO: I18n.
icon ?= drawIcon(resultColor, profileColor)
return {
title: chrome.i18n.getMessage('browserAction_titleWithResult', [
currentName
dispName(profile.name)
details
])
shortTitle = 'Omega: ' + currentName # TODO: I18n.
if profile.name != currentName
shortTitle += ' => ' + profile.name # TODO: I18n.
shortTitle: shortTitle
icon: icon
resultColor: resultColor
profileColor: profileColor
}
).catch -> return null
return {
title: chrome.i18n.getMessage('browserAction_titleWithResult', [
currentName
dispName(profile.name)
details
])
shortTitle: shortTitle
icon: icon
resultColor: resultColor
profileColor: profileColor
}
).catch -> return null
storage = new OmegaTargetCurrent.Storage('local')
state = new OmegaTargetCurrent.BrowserStorage(zeroLocalStorage, 'omega.local.')
storage = new OmegaTargetCurrent.Storage('local')
state = new OmegaTargetCurrent.BrowserStorage(zeroStorage, 'omega.local.')
if chrome?.storage?.sync or browser?.storage?.sync
syncStorage = new OmegaTargetCurrent.SyncStorage('sync', state)
sync = new OmegaTargetCurrent.OptionsSync(syncStorage)
sync.transformValue = OmegaTargetCurrent.Options.transformValueForSync
if chrome?.storage?.sync or browser?.storage?.sync
syncStorage = new OmegaTargetCurrent.SyncStorage('sync', state)
sync = new OmegaTargetCurrent.OptionsSync(syncStorage)
sync.transformValue = OmegaTargetCurrent.Options.transformValueForSync
proxyImpl = OmegaTargetCurrent.proxy.getProxyImpl(Log)
state.set({proxyImplFeatures: proxyImpl.features})
options = new OmegaTargetCurrent.Options(storage, state, Log, sync,
proxyImpl)
proxyImpl = OmegaTargetCurrent.proxy.getProxyImpl(Log)
state.set({proxyImplFeatures: proxyImpl.features})
options = new OmegaTargetCurrent.Options(storage, state, Log, sync,
proxyImpl)
options.initWithOptions(null, startupCheck)
options.initWithOptions(null, startupCheck)
options.externalApi = new OmegaTargetCurrent.ExternalApi(options)
options.externalApi.listen()
options.externalApi = new OmegaTargetCurrent.ExternalApi(options)
options.externalApi.listen()
if chrome.runtime.id != OmegaTargetCurrent.SwitchySharp.extId and false
options.switchySharp = new OmegaTargetCurrent.SwitchySharp()
options.switchySharp.monitor()
if chrome.runtime.id != OmegaTargetCurrent.SwitchySharp.extId and false
options.switchySharp = new OmegaTargetCurrent.SwitchySharp()
options.switchySharp.monitor()
tabs = new OmegaTargetCurrent.ChromeTabs(actionForUrl)
tabs.watch()
tabs = new OmegaTargetCurrent.ChromeTabs(actionForUrl)
tabs.watch()
options._inspect = new OmegaTargetCurrent.Inspect (url, tab) ->
if url == tab.url
options.clearBadge()
tabs.processTab(tab)
state.remove('inspectUrl')
return
state.set({inspectUrl: url})
actionForUrl(url).then (action) ->
return if not action
parsedUrl = OmegaTargetCurrent.Url.parse(url)
if parsedUrl.hostname == OmegaTargetCurrent.Url.parse(tab.url).hostname
urlDisp = parsedUrl.path
else
urlDisp = parsedUrl.hostname
title = chrome.i18n.getMessage('browserAction_titleInspect', urlDisp) + '\n'
title += action.title
chrome.action.setTitle(title: title, tabId: tab.id)
tabs.setTabBadge(tab, {
text: '#'
color: action.resultColor
})
options.setProxyNotControllable(null)
timeout = null
proxyImpl.watchProxyChange (details) ->
return if options.externalApi.disabled
return unless details
notControllableBefore = options.proxyNotControllable()
internal = false
noRevert = false
switch details['levelOfControl']
when "controlled_by_other_extensions", "not_controllable"
reason =
if details['levelOfControl'] == 'not_controllable'
'policy'
else
'app'
options.setProxyNotControllable(reason)
noRevert = true
else
options.setProxyNotControllable(null)
if details['levelOfControl'] == 'controlled_by_this_extension'
internal = true
return if not notControllableBefore
Log.log('external proxy: ', details)
# Chromium will send chrome.proxy.settings.onChange on extension unload,
# just after the current extension has lost control of the proxy settings.
# This is just annoying, and may change the currentProfileName state
# suprisingly.
# To workaround this issue, wait for some time before setting the proxy.
# However this will cause some delay before the settings are processed.
clearTimeout(timeout) if timeout?
parsed = null
timeout = setTimeout (->
if parsed
options.setExternalProfile(parsed,
{noRevert: noRevert, internal: internal})
), 500
parsed = proxyImpl.parseExternalProfile(details, options._options)
return
external = false
options.currentProfileChanged = (reason) ->
iconCache = {}
if reason == 'external'
external = true
else if reason != 'clearBadge'
external = false
current = options.currentProfile()
currentName = ''
if current
currentName = dispName(current.name)
if current.profileType == 'VirtualProfile'
realCurrentName = current.defaultProfileName
currentName += " [#{dispName(realCurrentName)}]"
current = options.profile(realCurrentName)
details = options.printProfile(current)
if currentName
title = chrome.i18n.getMessage('browserAction_titleWithResult', [
currentName, '', details])
shortTitle = 'Omega: ' + currentName # TODO: I18n.
else
title = details
shortTitle = 'Omega: ' + details # TODO: I18n.
if external and current.profileType != 'SystemProfile'
message = chrome.i18n.getMessage('browserAction_titleExternalProxy')
title = message + '\n' + title
shortTitle = 'Omega-Extern: ' + details # TODO: I18n.
options.setBadge()
if not current.name or not OmegaPac.Profiles.isInclusive(current)
icon = drawIcon(current.color)
else
icon = drawIcon(options.profile('direct').color, current.color)
tabs.resetAll(
icon: icon
title: title
shortTitle: shortTitle
)
encodeError = (obj) ->
if obj instanceof Error
{
_error: 'error'
name: obj.name
message: obj.message
stack: obj.stack
original: obj
}
else
obj
refreshActivePageIfEnabled = ->
return if zeroLocalStorage['omega.local.refreshOnProfileChange'] == 'false'
chrome.tabs.query {active: true, lastFocusedWindow: true}, (tabs) ->
url = tabs[0].pendingUrl or tabs[0].url
return if not url
return if url.substr(0, 6) == 'chrome'
return if url.substr(0, 6) == 'about:'
return if url.substr(0, 4) == 'moz-'
if tabs[0].pendingUrl
chrome.tabs.update(tabs[0].id, {url: url})
else
chrome.tabs.reload(tabs[0].id, {bypassCache: true})
resetAllOptions = ->
options.ready.then ->
options._watchStop?()
options._syncWatchStop?()
Promise.all([
chrome.storage.sync.clear(),
chrome.storage.local.clear()
])
chrome.runtime.onMessage.addListener (request, sender, respond) ->
return unless request and request.method
options.ready.then ->
if request.method == 'resetAllOptions'
target = globalThis
method = resetAllOptions
else if request.method == 'getState'
target = state
method = state.get
else if request.method == 'setState'
target = state
method = state.set
else
target = options
method = target[request.method]
if typeof method != 'function'
Log.error("No such method #{request.method}!")
respond(
error:
reason: 'noSuchMethod'
)
options._inspect = new OmegaTargetCurrent.Inspect (url, tab) ->
if url == tab.url
options.clearBadge()
tabs.processTab(tab)
state.remove('inspectUrl')
return
promise = Promise.resolve().then -> method.apply(target, request.args)
if request.refreshActivePage
promise.then refreshActivePageIfEnabled
return if request.noReply
state.set({inspectUrl: url})
promise.then (result) ->
if request.method == 'updateProfile'
for own key, value of result
result[key] = encodeError(value)
respond(result: result)
actionForUrl(url).then (action) ->
return if not action
parsedUrl = OmegaTargetCurrent.Url.parse(url)
if parsedUrl.hostname == OmegaTargetCurrent.Url.parse(tab.url).hostname
urlDisp = parsedUrl.path
else
urlDisp = parsedUrl.hostname
promise.catch (error) ->
Log.error(request.method + ' ==>', error)
respond(error: encodeError(error))
title = chrome.i18n.getMessage(
'browserAction_titleInspect', urlDisp) + '\n'
title += action.title
chrome.action.setTitle(title: title, tabId: tab.id)
tabs.setTabBadge(tab, {
text: '#'
color: action.resultColor
})
# Wait for my response!
return true unless request.noReply
options.setProxyNotControllable(null)
timeout = null
proxyImpl.watchProxyChange (details) ->
return if options.externalApi.disabled
return unless details
notControllableBefore = options.proxyNotControllable()
internal = false
noRevert = false
switch details['levelOfControl']
when "controlled_by_other_extensions", "not_controllable"
reason =
if details['levelOfControl'] == 'not_controllable'
'policy'
else
'app'
options.setProxyNotControllable(reason)
noRevert = true
else
options.setProxyNotControllable(null)
if details['levelOfControl'] == 'controlled_by_this_extension'
internal = true
return if not notControllableBefore
Log.log('external proxy: ', details)
# Chromium will send chrome.proxy.settings.onChange on extension unload,
# just after the current extension has lost control of the proxy settings.
# This is just annoying, and may change the currentProfileName state
# suprisingly.
# To workaround this issue, wait for some time before setting the proxy.
# However this will cause some delay before the settings are processed.
clearTimeout(timeout) if timeout?
parsed = null
timeout = setTimeout (->
if parsed
options.setExternalProfile(parsed,
{noRevert: noRevert, internal: internal})
), 500
parsed = proxyImpl.parseExternalProfile(details, options._options)
return
external = false
options.currentProfileChanged = (reason) ->
iconCache = {}
if reason == 'external'
external = true
else if reason != 'clearBadge'
external = false
current = options.currentProfile()
currentName = ''
if current
currentName = dispName(current.name)
if current.profileType == 'VirtualProfile'
realCurrentName = current.defaultProfileName
currentName += " [#{dispName(realCurrentName)}]"
current = options.profile(realCurrentName)
details = options.printProfile(current)
if currentName
title = chrome.i18n.getMessage('browserAction_titleWithResult', [
currentName, '', details])
shortTitle = 'Omega: ' + currentName # TODO: I18n.
else
title = details
shortTitle = 'Omega: ' + details # TODO: I18n.
if external and current.profileType != 'SystemProfile'
message = chrome.i18n.getMessage('browserAction_titleExternalProxy')
title = message + '\n' + title
shortTitle = 'Omega-Extern: ' + details # TODO: I18n.
options.setBadge()
if not current.name or not OmegaPac.Profiles.isInclusive(current)
icon = drawIcon(current.color)
else
icon = drawIcon(options.profile('direct').color, current.color)
tabs.resetAll(
icon: icon
title: title
shortTitle: shortTitle
)
encodeError = (obj) ->
if obj instanceof Error
{
_error: 'error'
name: obj.name
message: obj.message
stack: obj.stack
original: obj
}
else
obj
refreshActivePageIfEnabled = ->
return if zeroStorage['omega.local.refreshOnProfileChange'] == 'false'
chrome.tabs.query {active: true, lastFocusedWindow: true}, (tabs) ->
url = tabs[0].pendingUrl or tabs[0].url
return if not url
return if url.substr(0, 6) == 'chrome'
return if url.substr(0, 6) == 'about:'
return if url.substr(0, 4) == 'moz-'
if tabs[0].pendingUrl
chrome.tabs.update(tabs[0].id, {url: url})
else
chrome.tabs.reload(tabs[0].id, {bypassCache: true})
resetAllOptions = ->
options.ready.then ->
options._watchStop?()
options._syncWatchStop?()
Promise.all([
chrome.storage.sync.clear(),
chrome.storage.local.clear()
])
chrome.runtime.onMessage.addListener (request, sender, respond) ->
return unless request and request.method
options.ready.then ->
if request.method == 'resetAllOptions'
target = globalThis
method = resetAllOptions
else if request.method == 'getState'
target = state
method = state.get
else if request.method == 'setState'
target = state
method = state.set
else
target = options
method = target[request.method]
if typeof method != 'function'
Log.error("No such method #{request.method}!")
respond(
error:
reason: 'noSuchMethod'
)
return
promise = Promise.resolve().then -> method.apply(target, request.args)
if request.refreshActivePage
promise.then refreshActivePageIfEnabled
return if request.noReply
promise.then (result) ->
if request.method == 'updateProfile'
for own key, value of result
result[key] = encodeError(value)
respond(result: result)
promise.catch (error) ->
Log.error(request.method + ' ==>', error)
respond(error: encodeError(error))
# Wait for my response!
return true unless request.noReply
globalThis.zeroBackground = zeroBackground

View File

@ -262,32 +262,8 @@ class ChromeOptions extends OmegaTarget.Options
chrome.i18n.getMessage('browserAction_profileDetails_' + type) || null
upgrade: (options, changes) ->
super(options).catch (err) =>
return Promise.reject err if options?['schemaVersion']
getOldOptions = Promise.reject()
getOldOptions = getOldOptions.catch ->
if options?['config']
Promise.resolve options
else if localStorage['config']
Promise.resolve localStorage
else
Promise.reject new OmegaTarget.Options.NoOptionsError()
getOldOptions.then (oldOptions) =>
i18n = {
upgrade_profile_auto: chrome.i18n.getMessage('upgrade_profile_auto')
}
try
# Upgrade from SwitchySharp.
upgraded = require('./upgrade')(oldOptions, i18n)
catch ex
@log.error(ex)
return Promise.reject ex
if localStorage['config']
Object.getPrototypeOf(localStorage).clear.call(localStorage)
@_state.set({'firstRun': 'upgrade'})
return this && super(upgraded, upgraded)
super(options).catch (err) ->
return Promise.reject err
onFirstRun: (reason) ->
console.log('first run ....')

View File

@ -0,0 +1,471 @@
import FDBKeyRange from "./FDBKeyRange.js";
import FDBObjectStore from "./FDBObjectStore.js";
import cmp from "./lib/cmp.js";
import { DataError, InvalidAccessError, InvalidStateError, ReadOnlyError, TransactionInactiveError } from "./lib/errors.js";
import extractKey from "./lib/extractKey.js";
import valueToKey from "./lib/valueToKey.js";
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 = structuredClone(value);
if (effectiveObjectStore.keyPath !== null) {
let tempKey;
try {
tempKey = extractKey(effectiveObjectStore.keyPath, clone);
} 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
});
}
toString() {
return "[object IDBCursor]";
}
}
export default FDBCursor;

View File

@ -0,0 +1,11 @@
import FDBCursor from "./FDBCursor.js";
class FDBCursorWithValue extends FDBCursor {
value = undefined;
constructor(source, range, direction, request) {
super(source, range, direction, request);
}
toString() {
return "[object IDBCursorWithValue]";
}
}
export default FDBCursorWithValue;

View File

@ -0,0 +1,155 @@
import FDBTransaction from "./FDBTransaction.js";
import { ConstraintError, InvalidAccessError, InvalidStateError, NotFoundError, TransactionInactiveError } from "./lib/errors.js";
import FakeDOMStringList from "./lib/FakeDOMStringList.js";
import FakeEventTarget from "./lib/FakeEventTarget.js";
import ObjectStore from "./lib/ObjectStore.js";
import { queueTask } from "./lib/scheduling.js";
import validateKeyPath from "./lib/validateKeyPath.js";
const confirmActiveVersionchangeTransaction = database => {
if (!database._runningVersionchangeTransaction) {
throw new InvalidStateError();
}
// Find the latest versionchange transaction
const transactions = database._rawDatabase.transactions.filter(tx => {
return tx.mode === "versionchange";
});
const transaction = transactions[transactions.length - 1];
if (!transaction || transaction._state === "finished") {
throw new InvalidStateError();
}
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
return transaction;
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-closing-steps
const closeConnection = connection => {
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;
});
} else {
queueTask(() => {
closeConnection(connection);
});
}
};
// 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();
}
const objectStoreNames = [...this.objectStoreNames];
transaction._rollbackLog.push(() => {
const objectStore = this._rawDatabase.rawObjectStores.get(name);
if (objectStore) {
objectStore.deleted = true;
}
this.objectStoreNames = new FakeDOMStringList(...objectStoreNames);
transaction._scope.delete(name);
this._rawDatabase.rawObjectStores.delete(name);
});
const rawObjectStore = new ObjectStore(this._rawDatabase, name, keyPath, autoIncrement);
this.objectStoreNames._push(name);
this.objectStoreNames._sort();
transaction._scope.add(name);
this._rawDatabase.rawObjectStores.set(name, rawObjectStore);
transaction.objectStoreNames = new FakeDOMStringList(...this.objectStoreNames);
return transaction.objectStore(name);
}
deleteObjectStore(name) {
if (name === undefined) {
throw new TypeError();
}
const transaction = confirmActiveVersionchangeTransaction(this);
const store = this._rawDatabase.rawObjectStores.get(name);
if (store === undefined) {
throw new NotFoundError();
}
this.objectStoreNames = new FakeDOMStringList(...Array.from(this.objectStoreNames).filter(objectStoreName => {
return objectStoreName !== name;
}));
transaction.objectStoreNames = new FakeDOMStringList(...this.objectStoreNames);
transaction._rollbackLog.push(() => {
store.deleted = false;
this._rawDatabase.rawObjectStores.set(name, store);
this.objectStoreNames._push(name);
this.objectStoreNames._sort();
});
store.deleted = true;
this._rawDatabase.rawObjectStores.delete(name);
transaction._objectStoresCache.delete(name);
}
transaction(storeNames, mode) {
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");
}
}
const tx = new FDBTransaction(storeNames, mode, this);
this._rawDatabase.transactions.push(tx);
this._rawDatabase.processTransactions(); // See if can start right away (async)
return tx;
}
close() {
closeConnection(this);
}
toString() {
return "[object IDBDatabase]";
}
}
export default FDBDatabase;

View File

@ -0,0 +1,256 @@
import FDBDatabase from "./FDBDatabase.js";
import FDBOpenDBRequest from "./FDBOpenDBRequest.js";
import FDBVersionChangeEvent from "./FDBVersionChangeEvent.js";
import cmp from "./lib/cmp.js";
import Database from "./lib/Database.js";
import enforceRange from "./lib/enforceRange.js";
import { AbortError, VersionError } from "./lib/errors.js";
import FakeEvent from "./lib/FakeEvent.js";
import { queueTask } from "./lib/scheduling.js";
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);
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-deleting-a-database
const deleteDatabase = (databases, name, request, cb) => {
try {
const db = databases.get(name);
if (db === undefined) {
cb(null);
return;
}
db.deletePending = true;
const openDatabases = db.connections.filter(connection => {
return !connection._closed && !connection._closePending;
});
for (const openDatabase2 of openDatabases) {
if (!openDatabase2._closePending) {
const event = new FDBVersionChangeEvent("versionchange", {
newVersion: null,
oldVersion: db.version
});
openDatabase2.dispatchEvent(event);
}
}
const anyOpen = openDatabases.some(openDatabase3 => {
return !openDatabase3._closed && !openDatabase3._closePending;
});
if (request && anyOpen) {
const event = new FDBVersionChangeEvent("blocked", {
newVersion: null,
oldVersion: db.version
});
request.dispatchEvent(event);
}
waitForOthersClosedDelete(databases, name, openDatabases, cb);
} catch (err) {
cb(err);
}
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction
const runVersionchangeTransaction = (connection, version, request, cb) => {
connection._runningVersionchangeTransaction = true;
const oldVersion = connection.version;
const openDatabases = connection._rawDatabase.connections.filter(otherDatabase => {
return connection !== otherDatabase;
});
for (const openDatabase2 of openDatabases) {
if (!openDatabase2._closed && !openDatabase2._closePending) {
const event = new FDBVersionChangeEvent("versionchange", {
newVersion: version,
oldVersion
});
openDatabase2.dispatchEvent(event);
}
}
const anyOpen = openDatabases.some(openDatabase3 => {
return !openDatabase3._closed && !openDatabase3._closePending;
});
if (anyOpen) {
const event = new FDBVersionChangeEvent("blocked", {
newVersion: version,
oldVersion
});
request.dispatchEvent(event);
}
const waitForOthersClosed = () => {
const anyOpen2 = openDatabases.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(connection.objectStoreNames, "versionchange");
request.result = connection;
request.readyState = "done";
request.transaction = transaction;
transaction._rollbackLog.push(() => {
connection._rawDatabase.version = oldVersion;
connection.version = oldVersion;
});
const event = new FDBVersionChangeEvent("upgradeneeded", {
newVersion: version,
oldVersion
});
request.dispatchEvent(event);
transaction.addEventListener("error", () => {
connection._runningVersionchangeTransaction = false;
// 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.addEventListener("abort", () => {
connection._runningVersionchangeTransaction = false;
request.transaction = null;
queueTask(() => {
cb(new AbortError());
});
});
transaction.addEventListener("complete", () => {
connection._runningVersionchangeTransaction = false;
request.transaction = null;
// Let other complete event handlers run before continuing
queueTask(() => {
if (connection._closePending) {
cb(new AbortError());
} else {
cb(null);
}
});
});
};
waitForOthersClosed();
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-opening-a-database
const openDatabase = (databases, name, version, request, cb) => {
let db = databases.get(name);
if (db === undefined) {
db = new Database(name, 0);
databases.set(name, db);
}
if (version === undefined) {
version = db.version !== 0 ? db.version : 1;
}
if (db.version > version) {
return cb(new VersionError());
}
const connection = new FDBDatabase(db);
if (db.version < version) {
runVersionchangeTransaction(connection, version, request, err => {
if (err) {
// DO THIS HERE: ensure that connection is closed by running the steps for closing a database connection before these
// steps are aborted.
return cb(err);
}
cb(null, connection);
});
} else {
cb(null, connection);
}
};
class FDBFactory {
cmp = cmp;
_databases = new Map();
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBFactory-deleteDatabase-IDBOpenDBRequest-DOMString-name
deleteDatabase(name) {
const request = new FDBOpenDBRequest();
request.source = null;
queueTask(() => {
const db = this._databases.get(name);
const oldVersion = db !== undefined ? db.version : 0;
deleteDatabase(this._databases, name, request, err => {
if (err) {
request.error = new DOMException(err.message, err.name);
request.readyState = "done";
const event = new FakeEvent("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) {
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();
}
const request = new FDBOpenDBRequest();
request.source = null;
queueTask(() => {
openDatabase(this._databases, name, version, request, (err, connection) => {
if (err) {
request.result = undefined;
request.readyState = "done";
request.error = new DOMException(err.message, err.name);
const event = new FakeEvent("error", {
bubbles: true,
cancelable: true
});
event.eventPath = [];
request.dispatchEvent(event);
return;
}
request.result = connection;
request.readyState = "done";
const event2 = new FakeEvent("success");
event2.eventPath = [];
request.dispatchEvent(event2);
});
});
return request;
}
// https://w3c.github.io/IndexedDB/#dom-idbfactory-databases
databases() {
return new Promise(resolve => {
const result = [];
for (const [name, database] of this._databases) {
result.push({
name,
version: database.version
});
}
resolve(result);
});
}
toString() {
return "[object IDBFactory]";
}
}
export default FDBFactory;

View File

@ -0,0 +1,183 @@
import FDBCursor from "./FDBCursor.js";
import FDBCursorWithValue from "./FDBCursorWithValue.js";
import FDBKeyRange from "./FDBKeyRange.js";
import FDBRequest from "./FDBRequest.js";
import enforceRange from "./lib/enforceRange.js";
import { ConstraintError, InvalidStateError, TransactionInactiveError } from "./lib/errors.js";
import FakeDOMStringList from "./lib/FakeDOMStringList.js";
import valueToKey from "./lib/valueToKey.js";
import valueToKeyRange from "./lib/valueToKeyRange.js";
const confirmActiveTransaction = 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 = 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 new InvalidStateError();
}
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());
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(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(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(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(query, count) {
if (arguments.length > 1 && count !== undefined) {
count = enforceRange(count, "unsigned long");
}
confirmActiveTransaction(this);
const range = valueToKeyRange(query);
return this.objectStore.transaction._execRequestAsync({
operation: this._rawIndex.getAllValues.bind(this._rawIndex, range, count),
source: this
});
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-getKey-IDBRequest-any-key
getKey(key) {
confirmActiveTransaction(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(query, count) {
if (arguments.length > 1 && count !== undefined) {
count = enforceRange(count, "unsigned long");
}
confirmActiveTransaction(this);
const range = valueToKeyRange(query);
return this.objectStore.transaction._execRequestAsync({
operation: this._rawIndex.getAllKeys.bind(this._rawIndex, range, count),
source: this
});
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBIndex-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.objectStore.transaction._execRequestAsync({
operation: () => {
return this._rawIndex.count(key);
},
source: this
});
}
toString() {
return "[object IDBIndex]";
}
}
export default FDBIndex;

View File

@ -0,0 +1,71 @@
import cmp from "./lib/cmp.js";
import { DataError } from "./lib/errors.js";
import valueToKey from "./lib/valueToKey.js";
// 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;
}
toString() {
return "[object IDBKeyRange]";
}
}
export default FDBKeyRange;

View File

@ -0,0 +1,375 @@
import FDBCursor from "./FDBCursor.js";
import FDBCursorWithValue from "./FDBCursorWithValue.js";
import FDBIndex from "./FDBIndex.js";
import FDBKeyRange from "./FDBKeyRange.js";
import FDBRequest from "./FDBRequest.js";
import canInjectKey from "./lib/canInjectKey.js";
import enforceRange from "./lib/enforceRange.js";
import { ConstraintError, DataError, InvalidAccessError, InvalidStateError, NotFoundError, ReadOnlyError, TransactionInactiveError } from "./lib/errors.js";
import extractKey from "./lib/extractKey.js";
import FakeDOMStringList from "./lib/FakeDOMStringList.js";
import Index from "./lib/Index.js";
import validateKeyPath from "./lib/validateKeyPath.js";
import valueToKey from "./lib/valueToKey.js";
import valueToKeyRange from "./lib/valueToKeyRange.js";
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 = structuredClone(value);
if (objectStore.keyPath !== null) {
const tempKey = extractKey(objectStore.keyPath, clone);
if (tempKey !== undefined) {
valueToKey(tempKey);
} 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 = 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 new InvalidStateError();
}
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());
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(query, count) {
if (arguments.length > 1 && count !== undefined) {
count = enforceRange(count, "unsigned long");
}
confirmActiveTransaction(this);
const range = valueToKeyRange(query);
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.getAllValues.bind(this._rawObjectStore, range, count),
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(query, count) {
if (arguments.length > 1 && count !== undefined) {
count = enforceRange(count, "unsigned long");
}
confirmActiveTransaction(this);
const range = valueToKeyRange(query);
return this.transaction._execRequestAsync({
operation: this._rawObjectStore.getAllKeys.bind(this._rawObjectStore, range, count),
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.
const indexNames = [...this.indexNames];
this.transaction._rollbackLog.push(() => {
const index2 = this._rawObjectStore.rawIndexes.get(name);
if (index2) {
index2.deleted = true;
}
this.indexNames = new FakeDOMStringList(...indexNames);
this._rawObjectStore.rawIndexes.delete(name);
});
const index = new Index(this._rawObjectStore, name, keyPath, multiEntry, unique);
this.indexNames._push(name);
this.indexNames._sort();
this._rawObjectStore.rawIndexes.set(name, index);
index.initialize(this.transaction); // This is async by design
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(name, rawIndex);
this.indexNames._push(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
});
}
toString() {
return "[object IDBObjectStore]";
}
}
export default FDBObjectStore;

View File

@ -0,0 +1,9 @@
import FDBRequest from "./FDBRequest.js";
class FDBOpenDBRequest extends FDBRequest {
onupgradeneeded = null;
onblocked = null;
toString() {
return "[object IDBOpenDBRequest]";
}
}
export default FDBOpenDBRequest;

View File

@ -0,0 +1,33 @@
import { InvalidStateError } from "./lib/errors.js";
import FakeEventTarget from "./lib/FakeEventTarget.js";
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;
}
toString() {
return "[object IDBRequest]";
}
}
export default FDBRequest;

View File

@ -0,0 +1,213 @@
import FDBObjectStore from "./FDBObjectStore.js";
import FDBRequest from "./FDBRequest.js";
import { AbortError, InvalidStateError, NotFoundError, TransactionInactiveError } from "./lib/errors.js";
import FakeDOMStringList from "./lib/FakeDOMStringList.js";
import FakeEvent from "./lib/FakeEvent.js";
import FakeEventTarget from "./lib/FakeEventTarget.js";
import { queueTask } from "./lib/scheduling.js";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction
class FDBTransaction extends FakeEventTarget {
_state = "active";
_started = false;
_rollbackLog = [];
_objectStoresCache = new Map();
error = null;
onabort = null;
oncomplete = null;
onerror = null;
_requests = [];
constructor(storeNames, mode, db) {
super();
this._scope = new Set(storeNames);
this.mode = mode;
this.db = db;
this.objectStoreNames = new FakeDOMStringList(...Array.from(this._scope).sort());
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-aborting-a-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) {
request.result = undefined;
request.error = new AbortError();
const event = new FakeEvent("error", {
bubbles: true,
cancelable: true
});
event.eventPath = [this.db, this];
request.dispatchEvent(event);
}
}
}
queueTask(() => {
const event = new FakeEvent("abort", {
bubbles: true,
cancelable: false
});
event.eventPath = [this.db];
this.dispatchEvent(event);
});
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 FakeEvent("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 FakeEvent("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 !== "committing") {
this._abort("AbortError");
}
throw err;
}
// 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 FakeEvent("complete");
this.dispatchEvent(event);
}
}
}
commit() {
if (this._state !== "active") {
throw new InvalidStateError();
}
this._state = "committing";
}
toString() {
return "[object IDBRequest]";
}
}
export default FDBTransaction;

View File

@ -0,0 +1,12 @@
import FakeEvent from "./lib/FakeEvent.js";
class FDBVersionChangeEvent extends FakeEvent {
constructor(type, parameters = {}) {
super(type);
this.newVersion = parameters.newVersion !== undefined ? parameters.newVersion : null;
this.oldVersion = parameters.oldVersion !== undefined ? parameters.oldVersion : 0;
}
toString() {
return "[object IDBVersionChangeEvent]";
}
}
export default FDBVersionChangeEvent;

View File

@ -0,0 +1,3 @@
import FDBFactory from "./FDBFactory.js";
const fakeIndexedDB = new FDBFactory();
export default fakeIndexedDB;

View File

@ -0,0 +1,14 @@
import fakeIndexedDB from "./fakeIndexedDB.js";
export default fakeIndexedDB;
export { fakeIndexedDB as indexedDB };
export { default as IDBCursor } from "./FDBCursor.js";
export { default as IDBCursorWithValue } from "./FDBCursorWithValue.js";
export { default as IDBDatabase } from "./FDBDatabase.js";
export { default as IDBFactory } from "./FDBFactory.js";
export { default as IDBIndex } from "./FDBIndex.js";
export { default as IDBKeyRange } from "./FDBKeyRange.js";
export { default as IDBObjectStore } from "./FDBObjectStore.js";
export { default as IDBOpenDBRequest } from "./FDBOpenDBRequest.js";
export { default as IDBRequest } from "./FDBRequest.js";
export { default as IDBTransaction } from "./FDBTransaction.js";
export { default as IDBVersionChangeEvent } from "./FDBVersionChangeEvent.js";

View File

@ -0,0 +1,32 @@
import { queueTask } from "./scheduling.js";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-database
class Database {
deletePending = false;
transactions = [];
rawObjectStores = new Map();
connections = [];
constructor(name, version) {
this.name = name;
this.version = version;
this.processTransactions = this.processTransactions.bind(this);
}
processTransactions() {
queueTask(() => {
const anyRunning = this.transactions.some(transaction => {
return transaction._started && transaction._state !== "finished";
});
if (!anyRunning) {
const next = this.transactions.find(transaction => {
return !transaction._started && transaction._state !== "finished";
});
if (next) {
next.addEventListener("complete", this.processTransactions);
next.addEventListener("abort", this.processTransactions);
next._start();
}
}
});
}
}
export default Database;

View File

@ -0,0 +1,72 @@
class FakeDOMStringList extends Array {
contains(value) {
for (const value2 of this) {
if (value === value2) {
return true;
}
}
return false;
}
item(i) {
if (i < 0 || i >= this.length) {
return null;
}
return this[i];
}
// 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) {
return Array.prototype.push.call(this, ...values);
}
_sort(...values) {
return Array.prototype.sort.call(this, ...values);
}
}
// Would be nice to remove these properties to fix https://github.com/dumbmatter/fakeIndexedDB/issues/66 but for some reason it breaks Dexie - see test/dexie.js and FakeDOMStringList tests
/*
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array
const arrayPropertiesToDelete = ["from", "isArray", "of"];
const arrayMethodsToDelete = [
"at",
"concat",
"copyWithin",
"entries",
"every",
"fill",
"filter",
"find",
"findIndex",
"flat",
"flatMap",
"forEach",
"includes",
"indexOf",
"join",
"keys",
"lastIndexOf",
"map",
"pop",
"push",
"reduce",
"reduceRight",
"reverse",
"shift",
"slice",
"some",
"sort",
"splice",
"unshift",
"values",
];
// Set to undefined rather than delete, so it doesn't go up the chain to Array. Not perfect, but good enough?
for (const property of arrayPropertiesToDelete) {
(FakeDOMStringList as any)[property] = undefined;
}
for (const property of arrayMethodsToDelete) {
(FakeDOMStringList as any).prototype[property] = undefined;
}
*/
export default FakeDOMStringList;

View File

@ -0,0 +1,38 @@
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;
}
}
export default Event;

View File

@ -0,0 +1,104 @@
import { InvalidStateError } from "./errors.js";
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;
// 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;
}
// @ts-ignore
listener.callback.call(event.currentTarget, event);
}
const typeToProp = {
abort: "onabort",
blocked: "onblocked",
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)) {
// @ts-ignore
listener.callback.call(event.currentTarget, event);
}
}
};
class FakeEventTarget {
listeners = [];
// These will be overridden in individual subclasses and made not readonly
addEventListener(type, callback, capture = false) {
this.listeners.push({
callback,
capture,
type
});
}
removeEventListener(type, callback, capture = false) {
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;
}
}
export default FakeEventTarget;

View File

@ -0,0 +1,157 @@
import { ConstraintError } from "./errors.js";
import extractKey from "./extractKey.js";
import RecordStore from "./RecordStore.js";
import valueToKey from "./valueToKey.js";
// 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;
records = new RecordStore();
constructor(rawObjectStore, name, keyPath, multiEntry, unique) {
this.rawObjectStore = rawObjectStore;
this.name = name;
this.keyPath = keyPath;
this.multiEntry = multiEntry;
this.unique = 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) {
if (count === undefined || count === 0) {
count = Infinity;
}
const records = [];
for (const record of this.records.values(range)) {
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) {
if (count === undefined || count === 0) {
count = Infinity;
}
const records = [];
for (const record of this.records.values(range)) {
records.push(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);
} 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.add({
key: indexKey,
value: newRecord.key
});
} else {
for (const individualIndexKey of indexKey) {
this.records.add({
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;
}
}
export default Index;

View File

@ -0,0 +1,22 @@
import { ConstraintError } from "./errors.js";
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;
}
}
}
export default KeyGenerator;

View File

@ -0,0 +1,172 @@
import { ConstraintError, DataError } from "./errors.js";
import extractKey from "./extractKey.js";
import KeyGenerator from "./KeyGenerator.js";
import RecordStore from "./RecordStore.js";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-object-store
class ObjectStore {
deleted = false;
records = new RecordStore();
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) {
if (count === undefined || count === 0) {
count = Infinity;
}
const records = [];
for (const record of this.records.values(range)) {
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) {
if (count === undefined || count === 0) {
count = Infinity;
}
const records = [];
for (const record of this.records.values(range)) {
records.push(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);
if (key !== undefined) {
newRecord.key = key;
}
}
if (this.keyGenerator !== null && newRecord.key === undefined) {
if (rollbackLog) {
const keyGeneratorBefore = this.keyGenerator.num;
rollbackLog.push(() => {
if (this.keyGenerator) {
this.keyGenerator.num = keyGeneratorBefore;
}
});
}
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)) {
object[identifier] = {};
}
object = object[identifier];
}
}
identifier = remainingKeyPath;
object[identifier] = newRecord.key;
}
} else if (this.keyGenerator !== null && typeof newRecord.key === "number") {
this.keyGenerator.setIfLarger(newRecord.key);
}
const existingRecord = this.records.get(newRecord.key);
if (existingRecord) {
if (noOverwrite) {
throw new ConstraintError();
}
this.deleteRecord(newRecord.key, rollbackLog);
}
this.records.add(newRecord);
if (rollbackLog) {
rollbackLog.push(() => {
this.deleteRecord(newRecord.key);
});
}
// Update indexes
for (const rawIndex of this.rawIndexes.values()) {
if (rawIndex.initialized) {
rawIndex.storeRecord(newRecord);
}
}
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) {
let count = 0;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const record of this.records.values(range)) {
count += 1;
}
return count;
}
}
export default ObjectStore;

View File

@ -0,0 +1,133 @@
import FDBKeyRange from "../FDBKeyRange.js";
import { getByKey, getByKeyRange, getIndexByKey, getIndexByKeyGTE, getIndexByKeyRange } from "./binarySearch.js";
import cmp from "./cmp.js";
class RecordStore {
records = [];
get(key) {
if (key instanceof FDBKeyRange) {
return getByKeyRange(this.records, key);
}
return getByKey(this.records, key);
}
add(newRecord) {
// Find where to put it so it's sorted by key
let i;
if (this.records.length === 0) {
i = 0;
} else {
i = getIndexByKeyGTE(this.records, newRecord.key);
if (i === -1) {
// If no matching key, add to end
i = this.records.length;
} else {
// If matching key, advance to appropriate position based on value (used in indexes)
while (i < this.records.length && cmp(this.records[i].key, newRecord.key) === 0) {
if (cmp(this.records[i].value, newRecord.value) !== -1) {
// Record value >= newRecord value, so insert here
break;
}
i += 1; // Look at next record
}
}
}
this.records.splice(i, 0, newRecord);
}
delete(key) {
const deletedRecords = [];
const isRange = key instanceof FDBKeyRange;
while (true) {
const idx = isRange ? getIndexByKeyRange(this.records, key) : getIndexByKey(this.records, key);
if (idx === -1) {
break;
}
deletedRecords.push(this.records[idx]);
this.records.splice(idx, 1);
}
return deletedRecords;
}
deleteByValue(key) {
const range = key instanceof FDBKeyRange ? key : FDBKeyRange.only(key);
const deletedRecords = [];
this.records = this.records.filter(record => {
const shouldDelete = range.includes(record.value);
if (shouldDelete) {
deletedRecords.push(record);
}
return !shouldDelete;
});
return deletedRecords;
}
clear() {
const deletedRecords = this.records.slice();
this.records = [];
return deletedRecords;
}
values(range, direction = "next") {
return {
[Symbol.iterator]: () => {
let i;
if (direction === "next") {
i = 0;
if (range !== undefined && range.lower !== undefined) {
while (this.records[i] !== undefined) {
const cmpResult = cmp(this.records[i].key, range.lower);
if (cmpResult === 1 || cmpResult === 0 && !range.lowerOpen) {
break;
}
i += 1;
}
}
} else {
i = this.records.length - 1;
if (range !== undefined && range.upper !== undefined) {
while (this.records[i] !== undefined) {
const cmpResult = cmp(this.records[i].key, range.upper);
if (cmpResult === -1 || cmpResult === 0 && !range.upperOpen) {
break;
}
i -= 1;
}
}
}
return {
next: () => {
let done;
let value;
if (direction === "next") {
value = this.records[i];
done = i >= this.records.length;
i += 1;
if (!done && range !== undefined && range.upper !== undefined) {
const cmpResult = cmp(value.key, range.upper);
done = cmpResult === 1 || cmpResult === 0 && range.upperOpen;
if (done) {
value = undefined;
}
}
} else {
value = this.records[i];
done = i < 0;
i -= 1;
if (!done && range !== undefined && range.lower !== undefined) {
const cmpResult = cmp(value.key, range.lower);
done = cmpResult === -1 || cmpResult === 0 && range.lowerOpen;
if (done) {
value = undefined;
}
}
}
// The weird "as IteratorResult<Record>" is needed because of
// https://github.com/Microsoft/TypeScript/issues/11375 and
// https://github.com/Microsoft/TypeScript/issues/2983
return {
done,
value
};
}
};
}
};
}
}
export default RecordStore;

View File

@ -0,0 +1,74 @@
import cmp from "./cmp.js";
/**
* Classic binary search implementation. Returns the index where the key
* should be inserted, assuming the records list is ordered.
*/
function binarySearch(records, key) {
let low = 0;
let high = records.length;
let mid;
while (low < high) {
mid = low + high >>> 1; // like Math.floor((low + high) / 2) but fast
if (cmp(records[mid].key, key) < 0) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
/**
* Equivalent to `records.findIndex(record => cmp(record.key, key) === 0)`
*/
export function getIndexByKey(records, key) {
const idx = binarySearch(records, key);
const record = records[idx];
if (record && cmp(record.key, key) === 0) {
return idx;
}
return -1;
}
/**
* Equivalent to `records.find(record => cmp(record.key, key) === 0)`
*/
export function getByKey(records, key) {
const idx = getIndexByKey(records, key);
return records[idx];
}
/**
* Equivalent to `records.findIndex(record => key.includes(record.key))`
*/
export function getIndexByKeyRange(records, keyRange) {
const lowerIdx = typeof keyRange.lower === "undefined" ? 0 : binarySearch(records, keyRange.lower);
const upperIdx = typeof keyRange.upper === "undefined" ? records.length - 1 : binarySearch(records, keyRange.upper);
for (let i = lowerIdx; i <= upperIdx; i++) {
const record = records[i];
if (record && keyRange.includes(record.key)) {
return i;
}
}
return -1;
}
/**
* Equivalent to `records.find(record => key.includes(record.key))`
*/
export function getByKeyRange(records, keyRange) {
const idx = getIndexByKeyRange(records, keyRange);
return records[idx];
}
/**
* Equivalent to `records.findIndex(record => cmp(record.key, key) >= 0)`
*/
export function getIndexByKeyGTE(records, key) {
const idx = binarySearch(records, key);
const record = records[idx];
if (record && cmp(record.key, key) >= 0) {
return idx;
}
return -1;
}

View File

@ -0,0 +1,23 @@
// 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);
};
export default canInjectKey;

View File

@ -0,0 +1,77 @@
import { DataError } from "./errors.js";
import valueToKey from "./valueToKey.js";
const getType = x => {
if (typeof x === "number") {
return "Number";
}
if (x instanceof 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;
};
export default cmp;

View File

@ -0,0 +1,13 @@
// 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);
}
};
export default enforceRange;

View File

@ -0,0 +1,62 @@
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.",
DataCloneError: "The data being stored could not be cloned by the internal structured cloning algorithm.",
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."
};
export class AbortError extends DOMException {
constructor(message = messages.AbortError) {
super(message, "AbortError");
}
}
export class ConstraintError extends DOMException {
constructor(message = messages.ConstraintError) {
super(message, "ConstraintError");
}
}
export class DataCloneError extends DOMException {
constructor(message = messages.DataCloneError) {
super(message, "DataCloneError");
}
}
export class DataError extends DOMException {
constructor(message = messages.DataError) {
super(message, "DataError");
}
}
export class InvalidAccessError extends DOMException {
constructor(message = messages.InvalidAccessError) {
super(message, "InvalidAccessError");
}
}
export class InvalidStateError extends DOMException {
constructor(message = messages.InvalidStateError) {
super(message, "InvalidStateError");
}
}
export class NotFoundError extends DOMException {
constructor(message = messages.NotFoundError) {
super(message, "NotFoundError");
}
}
export class ReadOnlyError extends DOMException {
constructor(message = messages.ReadOnlyError) {
super(message, "ReadOnlyError");
}
}
export class TransactionInactiveError extends DOMException {
constructor(message = messages.TransactionInactiveError) {
super(message, "TransactionInactiveError");
}
}
export class VersionError extends DOMException {
constructor(message = messages.VersionError) {
super(message, "VersionError");
}
}

View File

@ -0,0 +1,39 @@
import valueToKey from "./valueToKey.js";
// 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();
}
result.push(valueToKey(extractKey(item, value)));
}
return result;
}
if (keyPath === "") {
return 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;
}
if (object === undefined || object === null || !Object.hasOwn(object, identifier)) {
return;
}
object = object[identifier];
}
return object;
};
export default extractKey;

View File

@ -0,0 +1,29 @@
// 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;
}
}
// 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.
export const queueTask = fn => {
const setImmediate = globalThis.setImmediate || getSetImmediateFromJsdom() || (fn => setTimeout(fn, 0));
setImmediate(fn);
};

View File

@ -0,0 +1 @@
export {};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,55 @@
import { DataError } from "./errors.js";
// https://w3c.github.io/IndexedDB/#convert-value-to-key
const valueToKey = (input, seen) => {
if (typeof input === "number") {
if (isNaN(input)) {
throw new DataError();
}
return input;
} else if (Object.prototype.toString.call(input) === "[object Date]") {
const ms = input.valueOf();
if (isNaN(ms)) {
throw new DataError();
}
return new Date(ms);
} else if (typeof input === "string") {
return input;
} else if (input instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && input instanceof SharedArrayBuffer || typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView && ArrayBuffer.isView(input)) {
let arrayBuffer;
let offset = 0;
let length = 0;
if (input instanceof ArrayBuffer || typeof SharedArrayBuffer !== "undefined" && input instanceof SharedArrayBuffer) {
arrayBuffer = input;
length = input.byteLength;
} else {
arrayBuffer = input.buffer;
offset = input.byteOffset;
length = input.byteLength;
}
if (arrayBuffer.detached) {
return new ArrayBuffer(0);
}
return arrayBuffer.slice(offset, offset + length);
} else if (Array.isArray(input)) {
if (seen === undefined) {
seen = new Set();
} else if (seen.has(input)) {
throw new DataError();
}
seen.add(input);
const keys = [];
for (let i = 0; i < input.length; i++) {
const hop = Object.hasOwn(input, i);
if (!hop) {
throw new DataError();
}
const entry = input[i];
const key = valueToKey(entry, seen);
keys.push(key);
}
return keys;
} else {
throw new DataError();
}
};
export default valueToKey;

View File

@ -0,0 +1,19 @@
import FDBKeyRange from "../FDBKeyRange.js";
import { DataError } from "./errors.js";
import valueToKey from "./valueToKey.js";
// 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);
};
export default valueToKeyRange;

View File

@ -1,5 +1,6 @@
window.onerror = (message, url, line, col, err) ->
console.log('globalThis onerror', arguments)
return unless globalThis.localStorage
log = localStorage['log'] || ''
if err?.stack
log += err.stack + '\n\n'