mirror of
https://github.com/zero-peak/ZeroOmega.git
synced 2025-02-02 02:58:13 -05:00
add firefox private mode suppport;add fake-indexeddb;
This commit is contained in:
parent
71e7bed82b
commit
fdfa1a25c3
67
omega-target-chromium-extension/overlay/indexedDB.js
Normal file
67
omega-target-chromium-extension/overlay/indexedDB.js
Normal 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
|
@ -1,4 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
/**
|
||||||
|
* author: suziwen1@gmail.com
|
||||||
|
**/
|
||||||
let valuesMap = new Map()
|
let valuesMap = new Map()
|
||||||
|
|
||||||
class LocalStorage {
|
class LocalStorage {
|
||||||
@ -42,7 +45,7 @@ class LocalStorage {
|
|||||||
}
|
}
|
||||||
const instance = new LocalStorage()
|
const instance = new LocalStorage()
|
||||||
|
|
||||||
globalThis.zeroLocalStorage = new Proxy(instance, {
|
const zeroLocalStorage = new Proxy(instance, {
|
||||||
set: function (obj, prop, value) {
|
set: function (obj, prop, value) {
|
||||||
if (LocalStorage.prototype.hasOwnProperty(prop)) {
|
if (LocalStorage.prototype.hasOwnProperty(prop)) {
|
||||||
instance[prop] = value
|
instance[prop] = value
|
||||||
@ -60,6 +63,5 @@ globalThis.zeroLocalStorage = new Proxy(instance, {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!globalThis.localStorage) {
|
|
||||||
globalThis.localStorage = globalThis.zeroLocalStorage;
|
export default zeroLocalStorage
|
||||||
}
|
|
||||||
|
@ -1,115 +1,119 @@
|
|||||||
const logStore = idbKeyval.createStore('log-store', 'log-store');
|
const ZeroLogFactory = ()=>{
|
||||||
|
const logStore = idbKeyval.createStore('log-store', 'log-store');
|
||||||
|
|
||||||
const logSequence = []
|
const logSequence = []
|
||||||
let isRunning = false
|
let isRunning = false
|
||||||
let splitStr = '\n----Z-e-r-o-O-m-e-g-a--------------\n'
|
let splitStr = '\n----Z-e-r-o-O-m-e-g-a--------------\n'
|
||||||
|
|
||||||
const originConsoleLog = console.log
|
const originConsoleLog = console.log
|
||||||
const originConsoleError = console.error
|
const originConsoleError = console.error
|
||||||
|
|
||||||
const _logFn = async function(){
|
const _logFn = async function(){
|
||||||
if (isRunning) return
|
if (isRunning) return
|
||||||
isRunning = true
|
isRunning = true
|
||||||
const _moment = moment()
|
const _moment = moment()
|
||||||
|
|
||||||
const dayOfWeek = _moment.format('E') // Day of Week (ISO), keep logs max 7 day
|
const dayOfWeek = _moment.format('E') // Day of Week (ISO), keep logs max 7 day
|
||||||
const monthNum = _moment.format('DD')
|
const monthNum = _moment.format('DD')
|
||||||
const logKey = 'zerolog-' + dayOfWeek
|
const logKey = 'zerolog-' + dayOfWeek
|
||||||
while (logSequence.length > 0) {
|
while (logSequence.length > 0) {
|
||||||
const str = logSequence.join('\n');
|
const str = logSequence.join('\n');
|
||||||
logSequence.length = 0;
|
logSequence.length = 0;
|
||||||
let logInfo = await idbKeyval.get(logKey, logStore)
|
let logInfo = await idbKeyval.get(logKey, logStore)
|
||||||
let date = _moment.format('YYYY-MM-DD')
|
let date = _moment.format('YYYY-MM-DD')
|
||||||
if (!logInfo || !logInfo.date) {
|
if (!logInfo || !logInfo.date) {
|
||||||
logInfo = { date: date, val: ''}
|
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)
|
|
||||||
}
|
}
|
||||||
} 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 {
|
try {
|
||||||
str = obj.toString()
|
if (typeof obj == 'string') {
|
||||||
|
str = obj
|
||||||
|
} else {
|
||||||
|
str = JSON.stringify(obj, replacerFn, 4)
|
||||||
|
}
|
||||||
} catch(e){
|
} catch(e){
|
||||||
|
try {
|
||||||
|
str = obj.toString()
|
||||||
|
} catch(e){
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return str
|
||||||
return str
|
})
|
||||||
})
|
return strArgs.join(' ')
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
_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 (){
|
export default ZeroLogFactory
|
||||||
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
|
|
||||||
|
@ -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 "./js/background_preload.js"
|
||||||
import "./lib/idb-keyval.js"
|
import "./lib/idb-keyval.js"
|
||||||
import "./lib/moment-with-locales.js"
|
import "./lib/moment-with-locales.js"
|
||||||
import "./localstorage-polyfill.js"
|
|
||||||
import "./lib/csso.js"
|
import "./lib/csso.js"
|
||||||
import "./js/log_error.js"
|
import "./js/log_error.js"
|
||||||
import "./log.js"
|
import "./log.js"
|
||||||
@ -11,4 +14,33 @@ import "./js/omega_pac.min.js"
|
|||||||
import "./js/omega_target.min.js"
|
import "./js/omega_target.min.js"
|
||||||
import "./js/omega_target_chromium_extension.min.js"
|
import "./js/omega_target_chromium_extension.min.js"
|
||||||
import "./img/icons/draw_omega.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)
|
||||||
|
})
|
||||||
|
@ -1,374 +1,380 @@
|
|||||||
OmegaTargetCurrent = Object.create(OmegaTargetChromium)
|
zeroBackground = (zeroStorage, opts) ->
|
||||||
Promise = OmegaTargetCurrent.Promise
|
OmegaTargetCurrent = Object.create(OmegaTargetChromium)
|
||||||
Promise.longStackTraces()
|
Promise = OmegaTargetCurrent.Promise
|
||||||
|
Promise.longStackTraces()
|
||||||
|
|
||||||
OmegaTargetCurrent.Log = Object.create(OmegaTargetCurrent.Log)
|
OmegaTargetCurrent.Log = Object.create(OmegaTargetCurrent.Log)
|
||||||
Log = OmegaTargetCurrent.Log
|
Log = OmegaTargetCurrent.Log
|
||||||
|
|
||||||
# TODO 将来可能代码需要重构下,这里写得有点乱. (suziwen1@gmail.com)
|
# TODO 将来可能代码需要重构下,这里写得有点乱. (suziwen1@gmail.com)
|
||||||
globalThis.isBrowserRestart = globalThis.startupCheck is undefined
|
globalThis.isBrowserRestart = globalThis.startupCheck is undefined
|
||||||
startupCheck = globalThis.startupCheck ?= -> true
|
startupCheck = globalThis.startupCheck ?= -> true
|
||||||
|
|
||||||
chrome.runtime.onStartup.addListener ->
|
chrome.runtime.onStartup.addListener ->
|
||||||
globalThis.isBrowserRestart = true
|
globalThis.isBrowserRestart = true
|
||||||
|
|
||||||
|
|
||||||
unhandledPromises = []
|
unhandledPromises = []
|
||||||
unhandledPromisesId = []
|
unhandledPromisesId = []
|
||||||
unhandledPromisesNextId = 1
|
unhandledPromisesNextId = 1
|
||||||
Promise.onPossiblyUnhandledRejection (reason, promise) ->
|
Promise.onPossiblyUnhandledRejection (reason, promise) ->
|
||||||
Log.error("[#{unhandledPromisesNextId}] Unhandled rejection:\n", reason)
|
Log.error("[#{unhandledPromisesNextId}] Unhandled rejection:\n", reason)
|
||||||
unhandledPromises.push(promise)
|
unhandledPromises.push(promise)
|
||||||
unhandledPromisesId.push(unhandledPromisesNextId)
|
unhandledPromisesId.push(unhandledPromisesNextId)
|
||||||
unhandledPromisesNextId++
|
unhandledPromisesNextId++
|
||||||
Promise.onUnhandledRejectionHandled (promise) ->
|
Promise.onUnhandledRejectionHandled (promise) ->
|
||||||
index = unhandledPromises.indexOf(promise)
|
index = unhandledPromises.indexOf(promise)
|
||||||
Log.log("[#{unhandledPromisesId[index]}] Rejection handled!", promise)
|
Log.log("[#{unhandledPromisesId[index]}] Rejection handled!", promise)
|
||||||
unhandledPromises.splice(index, 1)
|
unhandledPromises.splice(index, 1)
|
||||||
unhandledPromisesId.splice(index, 1)
|
unhandledPromisesId.splice(index, 1)
|
||||||
|
|
||||||
iconCache = {}
|
iconCache = {}
|
||||||
drawContext = null
|
drawContext = null
|
||||||
drawError = null
|
drawError = null
|
||||||
drawIcon = (resultColor, profileColor) ->
|
drawIcon = (resultColor, profileColor) ->
|
||||||
cacheKey = "omega+#{resultColor ? ''}+#{profileColor}"
|
cacheKey = "omega+#{resultColor ? ''}+#{profileColor}"
|
||||||
icon = iconCache[cacheKey]
|
icon = iconCache[cacheKey]
|
||||||
return icon if icon
|
return icon if icon
|
||||||
try
|
try
|
||||||
if not drawContext?
|
if not drawContext?
|
||||||
canvas = new OffscreenCanvas(300, 300)
|
canvas = new OffscreenCanvas(300, 300)
|
||||||
drawContext = canvas.getContext('2d', { willReadFrequently: true })
|
drawContext = canvas.getContext('2d', { willReadFrequently: true })
|
||||||
|
|
||||||
icon = {}
|
icon = {}
|
||||||
for size in [16, 19, 24, 32, 38]
|
for size in [16, 19, 24, 32, 38]
|
||||||
drawContext.scale(size, size)
|
drawContext.scale(size, size)
|
||||||
drawContext.clearRect(0, 0, 1, 1)
|
drawContext.clearRect(0, 0, 1, 1)
|
||||||
if resultColor?
|
if resultColor?
|
||||||
drawOmega drawContext, resultColor, profileColor
|
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"
|
|
||||||
else
|
else
|
||||||
condition = condition2Str(result[1].condition ? result[1])
|
drawOmega drawContext, profileColor
|
||||||
details += "#{condition} => "
|
drawContext.setTransform(1, 0, 0, 1, 0, 0)
|
||||||
if result[0] == 'DIRECT'
|
icon[size] = drawContext.getImageData(0, 0, size, size)
|
||||||
details += chrome.i18n.getMessage('browserAction_directResult')
|
if icon[size].data[3] == 255
|
||||||
details += '\n'
|
# Some browsers may replace the image data
|
||||||
direct = true
|
# 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
|
else
|
||||||
details += "#{result[0]}\n"
|
condition = condition2Str(result[1].condition ? result[1])
|
||||||
else if result.profileName
|
details += "#{condition} => "
|
||||||
if result.isTempRule
|
if result[0] == 'DIRECT'
|
||||||
details += chrome.i18n.getMessage('browserAction_tempRulePrefix')
|
details += chrome.i18n.getMessage('browserAction_directResult')
|
||||||
else if attached
|
details += '\n'
|
||||||
details += chrome.i18n.getMessage('browserAction_attachedPrefix')
|
direct = true
|
||||||
attached = false
|
else
|
||||||
condition = result.source ? condition2Str(result.condition)
|
details += "#{result[0]}\n"
|
||||||
details += "#{condition} => #{dispName(result.profileName)}\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
|
if not details
|
||||||
details = options.printProfile(current)
|
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
|
resultColor = profile.color
|
||||||
profileColor = current.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.
|
icon ?= drawIcon(resultColor, profileColor)
|
||||||
if profile.name != currentName
|
|
||||||
shortTitle += ' => ' + profile.name # TODO: I18n.
|
|
||||||
|
|
||||||
return {
|
shortTitle = 'Omega: ' + currentName # TODO: I18n.
|
||||||
title: chrome.i18n.getMessage('browserAction_titleWithResult', [
|
if profile.name != currentName
|
||||||
currentName
|
shortTitle += ' => ' + profile.name # TODO: I18n.
|
||||||
dispName(profile.name)
|
|
||||||
details
|
|
||||||
])
|
|
||||||
|
|
||||||
shortTitle: shortTitle
|
return {
|
||||||
icon: icon
|
title: chrome.i18n.getMessage('browserAction_titleWithResult', [
|
||||||
resultColor: resultColor
|
currentName
|
||||||
profileColor: profileColor
|
dispName(profile.name)
|
||||||
}
|
details
|
||||||
).catch -> return null
|
])
|
||||||
|
|
||||||
|
shortTitle: shortTitle
|
||||||
|
icon: icon
|
||||||
|
resultColor: resultColor
|
||||||
|
profileColor: profileColor
|
||||||
|
}
|
||||||
|
).catch -> return null
|
||||||
|
|
||||||
|
|
||||||
storage = new OmegaTargetCurrent.Storage('local')
|
storage = new OmegaTargetCurrent.Storage('local')
|
||||||
state = new OmegaTargetCurrent.BrowserStorage(zeroLocalStorage, 'omega.local.')
|
state = new OmegaTargetCurrent.BrowserStorage(zeroStorage, 'omega.local.')
|
||||||
|
|
||||||
if chrome?.storage?.sync or browser?.storage?.sync
|
if chrome?.storage?.sync or browser?.storage?.sync
|
||||||
syncStorage = new OmegaTargetCurrent.SyncStorage('sync', state)
|
syncStorage = new OmegaTargetCurrent.SyncStorage('sync', state)
|
||||||
sync = new OmegaTargetCurrent.OptionsSync(syncStorage)
|
sync = new OmegaTargetCurrent.OptionsSync(syncStorage)
|
||||||
sync.transformValue = OmegaTargetCurrent.Options.transformValueForSync
|
sync.transformValue = OmegaTargetCurrent.Options.transformValueForSync
|
||||||
|
|
||||||
proxyImpl = OmegaTargetCurrent.proxy.getProxyImpl(Log)
|
proxyImpl = OmegaTargetCurrent.proxy.getProxyImpl(Log)
|
||||||
state.set({proxyImplFeatures: proxyImpl.features})
|
state.set({proxyImplFeatures: proxyImpl.features})
|
||||||
options = new OmegaTargetCurrent.Options(storage, state, Log, sync,
|
options = new OmegaTargetCurrent.Options(storage, state, Log, sync,
|
||||||
proxyImpl)
|
proxyImpl)
|
||||||
|
|
||||||
options.initWithOptions(null, startupCheck)
|
options.initWithOptions(null, startupCheck)
|
||||||
|
|
||||||
options.externalApi = new OmegaTargetCurrent.ExternalApi(options)
|
options.externalApi = new OmegaTargetCurrent.ExternalApi(options)
|
||||||
options.externalApi.listen()
|
options.externalApi.listen()
|
||||||
|
|
||||||
if chrome.runtime.id != OmegaTargetCurrent.SwitchySharp.extId and false
|
if chrome.runtime.id != OmegaTargetCurrent.SwitchySharp.extId and false
|
||||||
options.switchySharp = new OmegaTargetCurrent.SwitchySharp()
|
options.switchySharp = new OmegaTargetCurrent.SwitchySharp()
|
||||||
options.switchySharp.monitor()
|
options.switchySharp.monitor()
|
||||||
|
|
||||||
tabs = new OmegaTargetCurrent.ChromeTabs(actionForUrl)
|
tabs = new OmegaTargetCurrent.ChromeTabs(actionForUrl)
|
||||||
tabs.watch()
|
tabs.watch()
|
||||||
|
|
||||||
options._inspect = new OmegaTargetCurrent.Inspect (url, tab) ->
|
options._inspect = new OmegaTargetCurrent.Inspect (url, tab) ->
|
||||||
if url == tab.url
|
if url == tab.url
|
||||||
options.clearBadge()
|
options.clearBadge()
|
||||||
tabs.processTab(tab)
|
tabs.processTab(tab)
|
||||||
state.remove('inspectUrl')
|
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'
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
promise = Promise.resolve().then -> method.apply(target, request.args)
|
state.set({inspectUrl: url})
|
||||||
if request.refreshActivePage
|
|
||||||
promise.then refreshActivePageIfEnabled
|
|
||||||
return if request.noReply
|
|
||||||
|
|
||||||
promise.then (result) ->
|
actionForUrl(url).then (action) ->
|
||||||
if request.method == 'updateProfile'
|
return if not action
|
||||||
for own key, value of result
|
parsedUrl = OmegaTargetCurrent.Url.parse(url)
|
||||||
result[key] = encodeError(value)
|
if parsedUrl.hostname == OmegaTargetCurrent.Url.parse(tab.url).hostname
|
||||||
respond(result: result)
|
urlDisp = parsedUrl.path
|
||||||
|
else
|
||||||
|
urlDisp = parsedUrl.hostname
|
||||||
|
|
||||||
promise.catch (error) ->
|
title = chrome.i18n.getMessage(
|
||||||
Log.error(request.method + ' ==>', error)
|
'browserAction_titleInspect', urlDisp) + '\n'
|
||||||
respond(error: encodeError(error))
|
title += action.title
|
||||||
|
chrome.action.setTitle(title: title, tabId: tab.id)
|
||||||
|
tabs.setTabBadge(tab, {
|
||||||
|
text: '#'
|
||||||
|
color: action.resultColor
|
||||||
|
})
|
||||||
|
|
||||||
# Wait for my response!
|
options.setProxyNotControllable(null)
|
||||||
return true unless request.noReply
|
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
|
||||||
|
@ -262,32 +262,8 @@ class ChromeOptions extends OmegaTarget.Options
|
|||||||
chrome.i18n.getMessage('browserAction_profileDetails_' + type) || null
|
chrome.i18n.getMessage('browserAction_profileDetails_' + type) || null
|
||||||
|
|
||||||
upgrade: (options, changes) ->
|
upgrade: (options, changes) ->
|
||||||
super(options).catch (err) =>
|
super(options).catch (err) ->
|
||||||
return Promise.reject err if options?['schemaVersion']
|
return Promise.reject err
|
||||||
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)
|
|
||||||
|
|
||||||
onFirstRun: (reason) ->
|
onFirstRun: (reason) ->
|
||||||
console.log('first run ....')
|
console.log('first run ....')
|
||||||
|
471
omega-web/lib/fake-indexeddb/FDBCursor.js
Normal file
471
omega-web/lib/fake-indexeddb/FDBCursor.js
Normal 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;
|
11
omega-web/lib/fake-indexeddb/FDBCursorWithValue.js
Normal file
11
omega-web/lib/fake-indexeddb/FDBCursorWithValue.js
Normal 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;
|
155
omega-web/lib/fake-indexeddb/FDBDatabase.js
Normal file
155
omega-web/lib/fake-indexeddb/FDBDatabase.js
Normal 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;
|
256
omega-web/lib/fake-indexeddb/FDBFactory.js
Normal file
256
omega-web/lib/fake-indexeddb/FDBFactory.js
Normal 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;
|
183
omega-web/lib/fake-indexeddb/FDBIndex.js
Normal file
183
omega-web/lib/fake-indexeddb/FDBIndex.js
Normal 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;
|
71
omega-web/lib/fake-indexeddb/FDBKeyRange.js
Normal file
71
omega-web/lib/fake-indexeddb/FDBKeyRange.js
Normal 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;
|
375
omega-web/lib/fake-indexeddb/FDBObjectStore.js
Normal file
375
omega-web/lib/fake-indexeddb/FDBObjectStore.js
Normal 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;
|
9
omega-web/lib/fake-indexeddb/FDBOpenDBRequest.js
Normal file
9
omega-web/lib/fake-indexeddb/FDBOpenDBRequest.js
Normal 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;
|
33
omega-web/lib/fake-indexeddb/FDBRequest.js
Normal file
33
omega-web/lib/fake-indexeddb/FDBRequest.js
Normal 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;
|
213
omega-web/lib/fake-indexeddb/FDBTransaction.js
Normal file
213
omega-web/lib/fake-indexeddb/FDBTransaction.js
Normal 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;
|
12
omega-web/lib/fake-indexeddb/FDBVersionChangeEvent.js
Normal file
12
omega-web/lib/fake-indexeddb/FDBVersionChangeEvent.js
Normal 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;
|
3
omega-web/lib/fake-indexeddb/fakeIndexedDB.js
Normal file
3
omega-web/lib/fake-indexeddb/fakeIndexedDB.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import FDBFactory from "./FDBFactory.js";
|
||||||
|
const fakeIndexedDB = new FDBFactory();
|
||||||
|
export default fakeIndexedDB;
|
14
omega-web/lib/fake-indexeddb/index.js
Normal file
14
omega-web/lib/fake-indexeddb/index.js
Normal 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";
|
32
omega-web/lib/fake-indexeddb/lib/Database.js
Normal file
32
omega-web/lib/fake-indexeddb/lib/Database.js
Normal 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;
|
72
omega-web/lib/fake-indexeddb/lib/FakeDOMStringList.js
Normal file
72
omega-web/lib/fake-indexeddb/lib/FakeDOMStringList.js
Normal 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;
|
38
omega-web/lib/fake-indexeddb/lib/FakeEvent.js
Normal file
38
omega-web/lib/fake-indexeddb/lib/FakeEvent.js
Normal 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;
|
104
omega-web/lib/fake-indexeddb/lib/FakeEventTarget.js
Normal file
104
omega-web/lib/fake-indexeddb/lib/FakeEventTarget.js
Normal 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;
|
157
omega-web/lib/fake-indexeddb/lib/Index.js
Normal file
157
omega-web/lib/fake-indexeddb/lib/Index.js
Normal 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;
|
22
omega-web/lib/fake-indexeddb/lib/KeyGenerator.js
Normal file
22
omega-web/lib/fake-indexeddb/lib/KeyGenerator.js
Normal 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;
|
172
omega-web/lib/fake-indexeddb/lib/ObjectStore.js
Normal file
172
omega-web/lib/fake-indexeddb/lib/ObjectStore.js
Normal 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;
|
133
omega-web/lib/fake-indexeddb/lib/RecordStore.js
Normal file
133
omega-web/lib/fake-indexeddb/lib/RecordStore.js
Normal 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;
|
74
omega-web/lib/fake-indexeddb/lib/binarySearch.js
Normal file
74
omega-web/lib/fake-indexeddb/lib/binarySearch.js
Normal 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;
|
||||||
|
}
|
23
omega-web/lib/fake-indexeddb/lib/canInjectKey.js
Normal file
23
omega-web/lib/fake-indexeddb/lib/canInjectKey.js
Normal 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;
|
77
omega-web/lib/fake-indexeddb/lib/cmp.js
Normal file
77
omega-web/lib/fake-indexeddb/lib/cmp.js
Normal 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;
|
13
omega-web/lib/fake-indexeddb/lib/enforceRange.js
Normal file
13
omega-web/lib/fake-indexeddb/lib/enforceRange.js
Normal 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;
|
62
omega-web/lib/fake-indexeddb/lib/errors.js
Normal file
62
omega-web/lib/fake-indexeddb/lib/errors.js
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
39
omega-web/lib/fake-indexeddb/lib/extractKey.js
Normal file
39
omega-web/lib/fake-indexeddb/lib/extractKey.js
Normal 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;
|
29
omega-web/lib/fake-indexeddb/lib/scheduling.js
Normal file
29
omega-web/lib/fake-indexeddb/lib/scheduling.js
Normal 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);
|
||||||
|
};
|
1
omega-web/lib/fake-indexeddb/lib/types.js
Normal file
1
omega-web/lib/fake-indexeddb/lib/types.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export {};
|
46
omega-web/lib/fake-indexeddb/lib/validateKeyPath.js
Normal file
46
omega-web/lib/fake-indexeddb/lib/validateKeyPath.js
Normal file
File diff suppressed because one or more lines are too long
55
omega-web/lib/fake-indexeddb/lib/valueToKey.js
Normal file
55
omega-web/lib/fake-indexeddb/lib/valueToKey.js
Normal 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;
|
19
omega-web/lib/fake-indexeddb/lib/valueToKeyRange.js
Normal file
19
omega-web/lib/fake-indexeddb/lib/valueToKeyRange.js
Normal 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;
|
@ -1,5 +1,6 @@
|
|||||||
window.onerror = (message, url, line, col, err) ->
|
window.onerror = (message, url, line, col, err) ->
|
||||||
console.log('globalThis onerror', arguments)
|
console.log('globalThis onerror', arguments)
|
||||||
|
return unless globalThis.localStorage
|
||||||
log = localStorage['log'] || ''
|
log = localStorage['log'] || ''
|
||||||
if err?.stack
|
if err?.stack
|
||||||
log += err.stack + '\n\n'
|
log += err.stack + '\n\n'
|
||||||
|
Loading…
Reference in New Issue
Block a user