mirror of
https://github.com/zero-peak/ZeroOmega.git
synced 2025-01-22 15:08:12 -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'
|
||||
/**
|
||||
* author: suziwen1@gmail.com
|
||||
**/
|
||||
let valuesMap = new Map()
|
||||
|
||||
class LocalStorage {
|
||||
@ -42,7 +45,7 @@ class LocalStorage {
|
||||
}
|
||||
const instance = new LocalStorage()
|
||||
|
||||
globalThis.zeroLocalStorage = new Proxy(instance, {
|
||||
const zeroLocalStorage = new Proxy(instance, {
|
||||
set: function (obj, prop, value) {
|
||||
if (LocalStorage.prototype.hasOwnProperty(prop)) {
|
||||
instance[prop] = value
|
||||
@ -60,6 +63,5 @@ globalThis.zeroLocalStorage = new Proxy(instance, {
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!globalThis.localStorage) {
|
||||
globalThis.localStorage = globalThis.zeroLocalStorage;
|
||||
}
|
||||
|
||||
export default zeroLocalStorage
|
||||
|
@ -1,115 +1,119 @@
|
||||
const logStore = idbKeyval.createStore('log-store', 'log-store');
|
||||
const ZeroLogFactory = ()=>{
|
||||
const logStore = idbKeyval.createStore('log-store', 'log-store');
|
||||
|
||||
const logSequence = []
|
||||
let isRunning = false
|
||||
let splitStr = '\n----Z-e-r-o-O-m-e-g-a--------------\n'
|
||||
const logSequence = []
|
||||
let isRunning = false
|
||||
let splitStr = '\n----Z-e-r-o-O-m-e-g-a--------------\n'
|
||||
|
||||
const originConsoleLog = console.log
|
||||
const originConsoleError = console.error
|
||||
const originConsoleLog = console.log
|
||||
const originConsoleError = console.error
|
||||
|
||||
const _logFn = async function(){
|
||||
if (isRunning) return
|
||||
isRunning = true
|
||||
const _moment = moment()
|
||||
const _logFn = async function(){
|
||||
if (isRunning) return
|
||||
isRunning = true
|
||||
const _moment = moment()
|
||||
|
||||
const dayOfWeek = _moment.format('E') // Day of Week (ISO), keep logs max 7 day
|
||||
const monthNum = _moment.format('DD')
|
||||
const logKey = 'zerolog-' + dayOfWeek
|
||||
while (logSequence.length > 0) {
|
||||
const str = logSequence.join('\n');
|
||||
logSequence.length = 0;
|
||||
let logInfo = await idbKeyval.get(logKey, logStore)
|
||||
let date = _moment.format('YYYY-MM-DD')
|
||||
if (!logInfo || !logInfo.date) {
|
||||
logInfo = { date: date, val: ''}
|
||||
}
|
||||
let { val } = logInfo
|
||||
if ( logInfo.date != date) {
|
||||
val = ''
|
||||
}
|
||||
val += splitStr
|
||||
splitStr = `\n`
|
||||
val += str
|
||||
await idbKeyval.set(logKey, { date, val }, logStore)
|
||||
}
|
||||
isRunning = false
|
||||
}
|
||||
|
||||
|
||||
const logFn = (str)=>{
|
||||
logSequence.push(moment().format('YYYY-MM-DD HH:mm:ss ') + ` ` + str)
|
||||
_logFn()
|
||||
}
|
||||
|
||||
const replacerFn = (key, value)=>{
|
||||
switch (key) {
|
||||
case 'username':
|
||||
case 'password':
|
||||
case 'host':
|
||||
case 'port':
|
||||
case 'token':
|
||||
case 'gistToken':
|
||||
case 'gistId':
|
||||
return '<secret>'
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const getStr = function (){
|
||||
const strArgs = [...arguments].map((obj)=>{
|
||||
let str = '';
|
||||
try {
|
||||
if (typeof obj == 'string') {
|
||||
str = obj
|
||||
} else {
|
||||
str = JSON.stringify(obj, replacerFn, 4)
|
||||
const dayOfWeek = _moment.format('E') // Day of Week (ISO), keep logs max 7 day
|
||||
const monthNum = _moment.format('DD')
|
||||
const logKey = 'zerolog-' + dayOfWeek
|
||||
while (logSequence.length > 0) {
|
||||
const str = logSequence.join('\n');
|
||||
logSequence.length = 0;
|
||||
let logInfo = await idbKeyval.get(logKey, logStore)
|
||||
let date = _moment.format('YYYY-MM-DD')
|
||||
if (!logInfo || !logInfo.date) {
|
||||
logInfo = { date: date, val: ''}
|
||||
}
|
||||
} catch(e){
|
||||
let { val } = logInfo
|
||||
if ( logInfo.date != date) {
|
||||
val = ''
|
||||
}
|
||||
val += splitStr
|
||||
splitStr = `\n`
|
||||
val += str
|
||||
await idbKeyval.set(logKey, { date, val }, logStore)
|
||||
}
|
||||
isRunning = false
|
||||
}
|
||||
|
||||
|
||||
const logFn = (str)=>{
|
||||
logSequence.push(moment().format('YYYY-MM-DD HH:mm:ss ') + ` ` + str)
|
||||
_logFn()
|
||||
}
|
||||
|
||||
const replacerFn = (key, value)=>{
|
||||
switch (key) {
|
||||
case 'username':
|
||||
case 'password':
|
||||
case 'host':
|
||||
case 'port':
|
||||
case 'token':
|
||||
case 'gistToken':
|
||||
case 'gistId':
|
||||
return '<secret>'
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const getStr = function (){
|
||||
const strArgs = [...arguments].map((obj)=>{
|
||||
let str = '';
|
||||
try {
|
||||
str = obj.toString()
|
||||
if (typeof obj == 'string') {
|
||||
str = obj
|
||||
} else {
|
||||
str = JSON.stringify(obj, replacerFn, 4)
|
||||
}
|
||||
} catch(e){
|
||||
try {
|
||||
str = obj.toString()
|
||||
} catch(e){
|
||||
}
|
||||
}
|
||||
}
|
||||
return str
|
||||
})
|
||||
return strArgs.join(' ')
|
||||
}
|
||||
|
||||
const ZeroLog = function(){
|
||||
logFn(getStr.apply(null, arguments))
|
||||
}
|
||||
|
||||
const _lastErrorLogFn = async ()=>{
|
||||
if (_lastErrorLogFn.isRunning) return
|
||||
_lastErrorLogFn.isRunning = true
|
||||
while (_lastErrorLogFn.val) {
|
||||
const val = _lastErrorLogFn.val
|
||||
_lastErrorLogFn.val = ''
|
||||
await idbKeyval.set('lastError', val, logStore)
|
||||
return str
|
||||
})
|
||||
return strArgs.join(' ')
|
||||
}
|
||||
_lastErrorLogFn.isRunning = false
|
||||
|
||||
const ZeroLog = function(){
|
||||
logFn(getStr.apply(null, arguments))
|
||||
}
|
||||
|
||||
const _lastErrorLogFn = async ()=>{
|
||||
if (_lastErrorLogFn.isRunning) return
|
||||
_lastErrorLogFn.isRunning = true
|
||||
while (_lastErrorLogFn.val) {
|
||||
const val = _lastErrorLogFn.val
|
||||
_lastErrorLogFn.val = ''
|
||||
await idbKeyval.set('lastError', val, logStore)
|
||||
}
|
||||
_lastErrorLogFn.isRunning = false
|
||||
}
|
||||
|
||||
const lastErrorLogFn = async function (){
|
||||
const val = getStr.apply(null, arguments)
|
||||
_lastErrorLogFn.val = val
|
||||
_lastErrorLogFn()
|
||||
}
|
||||
|
||||
const ZeroLogInfo = function() {
|
||||
originConsoleLog.apply(null, arguments)
|
||||
ZeroLog.apply(null, ['[INFO]', ...arguments])
|
||||
}
|
||||
const ZeroLogError = function(){
|
||||
originConsoleError.apply(null, arguments)
|
||||
ZeroLog.apply(null, ['[ERROR]', ...arguments])
|
||||
lastErrorLogFn.apply(null, arguments)
|
||||
}
|
||||
|
||||
const ZeroLogClear = async function(){
|
||||
await idbKeyval.clear(logStore)
|
||||
}
|
||||
|
||||
console.log = ZeroLogInfo
|
||||
console.error = ZeroLogError
|
||||
}
|
||||
|
||||
const lastErrorLogFn = async function (){
|
||||
const val = getStr.apply(null, arguments)
|
||||
_lastErrorLogFn.val = val
|
||||
_lastErrorLogFn()
|
||||
}
|
||||
|
||||
globalThis.ZeroLogInfo = function() {
|
||||
originConsoleLog.apply(null, arguments)
|
||||
ZeroLog.apply(null, ['[INFO]', ...arguments])
|
||||
}
|
||||
globalThis.ZeroLogError = function(){
|
||||
originConsoleError.apply(null, arguments)
|
||||
ZeroLog.apply(null, ['[ERROR]', ...arguments])
|
||||
lastErrorLogFn.apply(null, arguments)
|
||||
}
|
||||
|
||||
globalThis.ZeroLogClear = async function(){
|
||||
await idbKeyval.clear(logStore)
|
||||
}
|
||||
|
||||
console.log = ZeroLogInfo
|
||||
console.error = ZeroLogError
|
||||
export default ZeroLogFactory
|
||||
|
@ -1,7 +1,10 @@
|
||||
import zeroLocalStorage from "./localstorage-polyfill.js"
|
||||
import ZeroLogFactory from './log.js'
|
||||
import ZeroIndexedDBFactory from './indexedDB.js'
|
||||
|
||||
import "./js/background_preload.js"
|
||||
import "./lib/idb-keyval.js"
|
||||
import "./lib/moment-with-locales.js"
|
||||
import "./localstorage-polyfill.js"
|
||||
import "./lib/csso.js"
|
||||
import "./js/log_error.js"
|
||||
import "./log.js"
|
||||
@ -11,4 +14,33 @@ import "./js/omega_pac.min.js"
|
||||
import "./js/omega_target.min.js"
|
||||
import "./js/omega_target_chromium_extension.min.js"
|
||||
import "./img/icons/draw_omega.js"
|
||||
import "./js/background.js"
|
||||
import "./js/background.js" // zeroBackground
|
||||
|
||||
/**
|
||||
* author: suziwen1@gmail.com
|
||||
**/
|
||||
|
||||
const isFirefox = !!globalThis.localStorage
|
||||
|
||||
function detectPrivateMode(cb) {
|
||||
var db,
|
||||
on = cb.bind(null, true),
|
||||
off = cb.bind(null, false)
|
||||
if (isFirefox) {
|
||||
db = indexedDB.open("zeroOmega-test"), db.onerror = on, db.onsuccess = off
|
||||
} else {
|
||||
off()
|
||||
}
|
||||
}
|
||||
|
||||
detectPrivateMode(function (isPrivateMode) {
|
||||
|
||||
if (isPrivateMode && isFirefox) {
|
||||
// fake indexedDB
|
||||
ZeroIndexedDBFactory()
|
||||
}
|
||||
ZeroLogFactory()
|
||||
const zeroStorage = isFirefox ? localStorage : zeroLocalStorage
|
||||
globalThis.zeroBackground(zeroStorage)
|
||||
console.log('is private mode: ' + isPrivateMode)
|
||||
})
|
||||
|
@ -1,374 +1,380 @@
|
||||
OmegaTargetCurrent = Object.create(OmegaTargetChromium)
|
||||
Promise = OmegaTargetCurrent.Promise
|
||||
Promise.longStackTraces()
|
||||
zeroBackground = (zeroStorage, opts) ->
|
||||
OmegaTargetCurrent = Object.create(OmegaTargetChromium)
|
||||
Promise = OmegaTargetCurrent.Promise
|
||||
Promise.longStackTraces()
|
||||
|
||||
OmegaTargetCurrent.Log = Object.create(OmegaTargetCurrent.Log)
|
||||
Log = OmegaTargetCurrent.Log
|
||||
OmegaTargetCurrent.Log = Object.create(OmegaTargetCurrent.Log)
|
||||
Log = OmegaTargetCurrent.Log
|
||||
|
||||
# TODO 将来可能代码需要重构下,这里写得有点乱. (suziwen1@gmail.com)
|
||||
globalThis.isBrowserRestart = globalThis.startupCheck is undefined
|
||||
startupCheck = globalThis.startupCheck ?= -> true
|
||||
globalThis.isBrowserRestart = globalThis.startupCheck is undefined
|
||||
startupCheck = globalThis.startupCheck ?= -> true
|
||||
|
||||
chrome.runtime.onStartup.addListener ->
|
||||
globalThis.isBrowserRestart = true
|
||||
chrome.runtime.onStartup.addListener ->
|
||||
globalThis.isBrowserRestart = true
|
||||
|
||||
|
||||
unhandledPromises = []
|
||||
unhandledPromisesId = []
|
||||
unhandledPromisesNextId = 1
|
||||
Promise.onPossiblyUnhandledRejection (reason, promise) ->
|
||||
Log.error("[#{unhandledPromisesNextId}] Unhandled rejection:\n", reason)
|
||||
unhandledPromises.push(promise)
|
||||
unhandledPromisesId.push(unhandledPromisesNextId)
|
||||
unhandledPromisesNextId++
|
||||
Promise.onUnhandledRejectionHandled (promise) ->
|
||||
index = unhandledPromises.indexOf(promise)
|
||||
Log.log("[#{unhandledPromisesId[index]}] Rejection handled!", promise)
|
||||
unhandledPromises.splice(index, 1)
|
||||
unhandledPromisesId.splice(index, 1)
|
||||
unhandledPromises = []
|
||||
unhandledPromisesId = []
|
||||
unhandledPromisesNextId = 1
|
||||
Promise.onPossiblyUnhandledRejection (reason, promise) ->
|
||||
Log.error("[#{unhandledPromisesNextId}] Unhandled rejection:\n", reason)
|
||||
unhandledPromises.push(promise)
|
||||
unhandledPromisesId.push(unhandledPromisesNextId)
|
||||
unhandledPromisesNextId++
|
||||
Promise.onUnhandledRejectionHandled (promise) ->
|
||||
index = unhandledPromises.indexOf(promise)
|
||||
Log.log("[#{unhandledPromisesId[index]}] Rejection handled!", promise)
|
||||
unhandledPromises.splice(index, 1)
|
||||
unhandledPromisesId.splice(index, 1)
|
||||
|
||||
iconCache = {}
|
||||
drawContext = null
|
||||
drawError = null
|
||||
drawIcon = (resultColor, profileColor) ->
|
||||
cacheKey = "omega+#{resultColor ? ''}+#{profileColor}"
|
||||
icon = iconCache[cacheKey]
|
||||
return icon if icon
|
||||
try
|
||||
if not drawContext?
|
||||
canvas = new OffscreenCanvas(300, 300)
|
||||
drawContext = canvas.getContext('2d', { willReadFrequently: true })
|
||||
iconCache = {}
|
||||
drawContext = null
|
||||
drawError = null
|
||||
drawIcon = (resultColor, profileColor) ->
|
||||
cacheKey = "omega+#{resultColor ? ''}+#{profileColor}"
|
||||
icon = iconCache[cacheKey]
|
||||
return icon if icon
|
||||
try
|
||||
if not drawContext?
|
||||
canvas = new OffscreenCanvas(300, 300)
|
||||
drawContext = canvas.getContext('2d', { willReadFrequently: true })
|
||||
|
||||
icon = {}
|
||||
for size in [16, 19, 24, 32, 38]
|
||||
drawContext.scale(size, size)
|
||||
drawContext.clearRect(0, 0, 1, 1)
|
||||
if resultColor?
|
||||
drawOmega drawContext, resultColor, profileColor
|
||||
else
|
||||
drawOmega drawContext, profileColor
|
||||
drawContext.setTransform(1, 0, 0, 1, 0, 0)
|
||||
icon[size] = drawContext.getImageData(0, 0, size, size)
|
||||
if icon[size].data[3] == 255
|
||||
# Some browsers may replace the image data with a opaque white image to
|
||||
# resist fingerprinting. In that case the icon cannot be drawn.
|
||||
throw new Error('Icon drawing blocked by privacy.resistFingerprinting.')
|
||||
catch e
|
||||
if not drawError?
|
||||
drawError = e
|
||||
Log.error(e)
|
||||
Log.error('Profile-colored icon disabled. Falling back to static icon.')
|
||||
icon = null
|
||||
|
||||
return iconCache[cacheKey] = icon
|
||||
|
||||
charCodeUnderscore = '_'.charCodeAt(0)
|
||||
isHidden = (name) -> (name.charCodeAt(0) == charCodeUnderscore and
|
||||
name.charCodeAt(1) == charCodeUnderscore)
|
||||
|
||||
dispName = (name) -> chrome.i18n.getMessage('profile_' + name) || name
|
||||
|
||||
actionForUrl = (url) ->
|
||||
options.ready.then(->
|
||||
request = OmegaPac.Conditions.requestFromUrl(url)
|
||||
options.matchProfile(request)
|
||||
).then(({profile, results}) ->
|
||||
current = options.currentProfile()
|
||||
currentName = dispName(current.name)
|
||||
if current.profileType == 'VirtualProfile'
|
||||
realCurrentName = current.defaultProfileName
|
||||
currentName += " [#{dispName(realCurrentName)}]"
|
||||
current = options.profile(realCurrentName)
|
||||
details = ''
|
||||
direct = false
|
||||
attached = false
|
||||
condition2Str = (condition) ->
|
||||
condition.pattern || OmegaPac.Conditions.str(condition)
|
||||
for result in results
|
||||
if Array.isArray(result)
|
||||
if not result[1]?
|
||||
attached = false
|
||||
name = result[0]
|
||||
if name[0] == '+'
|
||||
name = name.substr(1)
|
||||
if isHidden(name)
|
||||
attached = true
|
||||
else if name != realCurrentName
|
||||
details += chrome.i18n.getMessage 'browserAction_defaultRuleDetails'
|
||||
details += " => #{dispName(name)}\n"
|
||||
else if result[1].length == 0
|
||||
if result[0] == 'DIRECT'
|
||||
details += chrome.i18n.getMessage('browserAction_directResult')
|
||||
details += '\n'
|
||||
direct = true
|
||||
else
|
||||
details += "#{result[0]}\n"
|
||||
else if typeof result[1] == 'string'
|
||||
details += "#{result[1]} => #{result[0]}\n"
|
||||
icon = {}
|
||||
for size in [16, 19, 24, 32, 38]
|
||||
drawContext.scale(size, size)
|
||||
drawContext.clearRect(0, 0, 1, 1)
|
||||
if resultColor?
|
||||
drawOmega drawContext, resultColor, profileColor
|
||||
else
|
||||
condition = condition2Str(result[1].condition ? result[1])
|
||||
details += "#{condition} => "
|
||||
if result[0] == 'DIRECT'
|
||||
details += chrome.i18n.getMessage('browserAction_directResult')
|
||||
details += '\n'
|
||||
direct = true
|
||||
drawOmega drawContext, profileColor
|
||||
drawContext.setTransform(1, 0, 0, 1, 0, 0)
|
||||
icon[size] = drawContext.getImageData(0, 0, size, size)
|
||||
if icon[size].data[3] == 255
|
||||
# Some browsers may replace the image data
|
||||
# with a opaque white image to
|
||||
# resist fingerprinting. In that case the icon cannot be drawn.
|
||||
throw new Error(
|
||||
'Icon drawing blocked by privacy.resistFingerprinting.')
|
||||
catch e
|
||||
if not drawError?
|
||||
drawError = e
|
||||
Log.error(e)
|
||||
Log.error('Profile-colored icon disabled. Falling back to static icon.')
|
||||
icon = null
|
||||
|
||||
return iconCache[cacheKey] = icon
|
||||
|
||||
charCodeUnderscore = '_'.charCodeAt(0)
|
||||
isHidden = (name) -> (name.charCodeAt(0) == charCodeUnderscore and
|
||||
name.charCodeAt(1) == charCodeUnderscore)
|
||||
|
||||
dispName = (name) -> chrome.i18n.getMessage('profile_' + name) || name
|
||||
|
||||
actionForUrl = (url) ->
|
||||
options.ready.then(->
|
||||
request = OmegaPac.Conditions.requestFromUrl(url)
|
||||
options.matchProfile(request)
|
||||
).then(({profile, results}) ->
|
||||
current = options.currentProfile()
|
||||
currentName = dispName(current.name)
|
||||
if current.profileType == 'VirtualProfile'
|
||||
realCurrentName = current.defaultProfileName
|
||||
currentName += " [#{dispName(realCurrentName)}]"
|
||||
current = options.profile(realCurrentName)
|
||||
details = ''
|
||||
direct = false
|
||||
attached = false
|
||||
condition2Str = (condition) ->
|
||||
condition.pattern || OmegaPac.Conditions.str(condition)
|
||||
for result in results
|
||||
if Array.isArray(result)
|
||||
if not result[1]?
|
||||
attached = false
|
||||
name = result[0]
|
||||
if name[0] == '+'
|
||||
name = name.substr(1)
|
||||
if isHidden(name)
|
||||
attached = true
|
||||
else if name != realCurrentName
|
||||
details +=
|
||||
chrome.i18n.getMessage 'browserAction_defaultRuleDetails'
|
||||
details += " => #{dispName(name)}\n"
|
||||
else if result[1].length == 0
|
||||
if result[0] == 'DIRECT'
|
||||
details += chrome.i18n.getMessage('browserAction_directResult')
|
||||
details += '\n'
|
||||
direct = true
|
||||
else
|
||||
details += "#{result[0]}\n"
|
||||
else if typeof result[1] == 'string'
|
||||
details += "#{result[1]} => #{result[0]}\n"
|
||||
else
|
||||
details += "#{result[0]}\n"
|
||||
else if result.profileName
|
||||
if result.isTempRule
|
||||
details += chrome.i18n.getMessage('browserAction_tempRulePrefix')
|
||||
else if attached
|
||||
details += chrome.i18n.getMessage('browserAction_attachedPrefix')
|
||||
attached = false
|
||||
condition = result.source ? condition2Str(result.condition)
|
||||
details += "#{condition} => #{dispName(result.profileName)}\n"
|
||||
condition = condition2Str(result[1].condition ? result[1])
|
||||
details += "#{condition} => "
|
||||
if result[0] == 'DIRECT'
|
||||
details += chrome.i18n.getMessage('browserAction_directResult')
|
||||
details += '\n'
|
||||
direct = true
|
||||
else
|
||||
details += "#{result[0]}\n"
|
||||
else if result.profileName
|
||||
if result.isTempRule
|
||||
details += chrome.i18n.getMessage('browserAction_tempRulePrefix')
|
||||
else if attached
|
||||
details += chrome.i18n.getMessage('browserAction_attachedPrefix')
|
||||
attached = false
|
||||
condition = result.source ? condition2Str(result.condition)
|
||||
details += "#{condition} => #{dispName(result.profileName)}\n"
|
||||
|
||||
if not details
|
||||
details = options.printProfile(current)
|
||||
if not details
|
||||
details = options.printProfile(current)
|
||||
|
||||
resultColor = profile.color
|
||||
profileColor = current.color
|
||||
|
||||
icon = null
|
||||
if direct
|
||||
resultColor = options.profile('direct').color
|
||||
profileColor = profile.color
|
||||
else if profile.name == current.name and options.isCurrentProfileStatic()
|
||||
resultColor = profileColor = profile.color
|
||||
icon = drawIcon(profile.color)
|
||||
else
|
||||
resultColor = profile.color
|
||||
profileColor = current.color
|
||||
|
||||
icon ?= drawIcon(resultColor, profileColor)
|
||||
icon = null
|
||||
if direct
|
||||
resultColor = options.profile('direct').color
|
||||
profileColor = profile.color
|
||||
else if profile.name == current.name and options.isCurrentProfileStatic()
|
||||
resultColor = profileColor = profile.color
|
||||
icon = drawIcon(profile.color)
|
||||
else
|
||||
resultColor = profile.color
|
||||
profileColor = current.color
|
||||
|
||||
shortTitle = 'Omega: ' + currentName # TODO: I18n.
|
||||
if profile.name != currentName
|
||||
shortTitle += ' => ' + profile.name # TODO: I18n.
|
||||
icon ?= drawIcon(resultColor, profileColor)
|
||||
|
||||
return {
|
||||
title: chrome.i18n.getMessage('browserAction_titleWithResult', [
|
||||
currentName
|
||||
dispName(profile.name)
|
||||
details
|
||||
])
|
||||
shortTitle = 'Omega: ' + currentName # TODO: I18n.
|
||||
if profile.name != currentName
|
||||
shortTitle += ' => ' + profile.name # TODO: I18n.
|
||||
|
||||
shortTitle: shortTitle
|
||||
icon: icon
|
||||
resultColor: resultColor
|
||||
profileColor: profileColor
|
||||
}
|
||||
).catch -> return null
|
||||
return {
|
||||
title: chrome.i18n.getMessage('browserAction_titleWithResult', [
|
||||
currentName
|
||||
dispName(profile.name)
|
||||
details
|
||||
])
|
||||
|
||||
shortTitle: shortTitle
|
||||
icon: icon
|
||||
resultColor: resultColor
|
||||
profileColor: profileColor
|
||||
}
|
||||
).catch -> return null
|
||||
|
||||
|
||||
storage = new OmegaTargetCurrent.Storage('local')
|
||||
state = new OmegaTargetCurrent.BrowserStorage(zeroLocalStorage, 'omega.local.')
|
||||
storage = new OmegaTargetCurrent.Storage('local')
|
||||
state = new OmegaTargetCurrent.BrowserStorage(zeroStorage, 'omega.local.')
|
||||
|
||||
if chrome?.storage?.sync or browser?.storage?.sync
|
||||
syncStorage = new OmegaTargetCurrent.SyncStorage('sync', state)
|
||||
sync = new OmegaTargetCurrent.OptionsSync(syncStorage)
|
||||
sync.transformValue = OmegaTargetCurrent.Options.transformValueForSync
|
||||
if chrome?.storage?.sync or browser?.storage?.sync
|
||||
syncStorage = new OmegaTargetCurrent.SyncStorage('sync', state)
|
||||
sync = new OmegaTargetCurrent.OptionsSync(syncStorage)
|
||||
sync.transformValue = OmegaTargetCurrent.Options.transformValueForSync
|
||||
|
||||
proxyImpl = OmegaTargetCurrent.proxy.getProxyImpl(Log)
|
||||
state.set({proxyImplFeatures: proxyImpl.features})
|
||||
options = new OmegaTargetCurrent.Options(storage, state, Log, sync,
|
||||
proxyImpl)
|
||||
proxyImpl = OmegaTargetCurrent.proxy.getProxyImpl(Log)
|
||||
state.set({proxyImplFeatures: proxyImpl.features})
|
||||
options = new OmegaTargetCurrent.Options(storage, state, Log, sync,
|
||||
proxyImpl)
|
||||
|
||||
options.initWithOptions(null, startupCheck)
|
||||
options.initWithOptions(null, startupCheck)
|
||||
|
||||
options.externalApi = new OmegaTargetCurrent.ExternalApi(options)
|
||||
options.externalApi.listen()
|
||||
options.externalApi = new OmegaTargetCurrent.ExternalApi(options)
|
||||
options.externalApi.listen()
|
||||
|
||||
if chrome.runtime.id != OmegaTargetCurrent.SwitchySharp.extId and false
|
||||
options.switchySharp = new OmegaTargetCurrent.SwitchySharp()
|
||||
options.switchySharp.monitor()
|
||||
if chrome.runtime.id != OmegaTargetCurrent.SwitchySharp.extId and false
|
||||
options.switchySharp = new OmegaTargetCurrent.SwitchySharp()
|
||||
options.switchySharp.monitor()
|
||||
|
||||
tabs = new OmegaTargetCurrent.ChromeTabs(actionForUrl)
|
||||
tabs.watch()
|
||||
tabs = new OmegaTargetCurrent.ChromeTabs(actionForUrl)
|
||||
tabs.watch()
|
||||
|
||||
options._inspect = new OmegaTargetCurrent.Inspect (url, tab) ->
|
||||
if url == tab.url
|
||||
options.clearBadge()
|
||||
tabs.processTab(tab)
|
||||
state.remove('inspectUrl')
|
||||
return
|
||||
|
||||
state.set({inspectUrl: url})
|
||||
|
||||
actionForUrl(url).then (action) ->
|
||||
return if not action
|
||||
parsedUrl = OmegaTargetCurrent.Url.parse(url)
|
||||
if parsedUrl.hostname == OmegaTargetCurrent.Url.parse(tab.url).hostname
|
||||
urlDisp = parsedUrl.path
|
||||
else
|
||||
urlDisp = parsedUrl.hostname
|
||||
|
||||
title = chrome.i18n.getMessage('browserAction_titleInspect', urlDisp) + '\n'
|
||||
title += action.title
|
||||
chrome.action.setTitle(title: title, tabId: tab.id)
|
||||
tabs.setTabBadge(tab, {
|
||||
text: '#'
|
||||
color: action.resultColor
|
||||
})
|
||||
|
||||
options.setProxyNotControllable(null)
|
||||
timeout = null
|
||||
|
||||
proxyImpl.watchProxyChange (details) ->
|
||||
return if options.externalApi.disabled
|
||||
return unless details
|
||||
notControllableBefore = options.proxyNotControllable()
|
||||
internal = false
|
||||
noRevert = false
|
||||
switch details['levelOfControl']
|
||||
when "controlled_by_other_extensions", "not_controllable"
|
||||
reason =
|
||||
if details['levelOfControl'] == 'not_controllable'
|
||||
'policy'
|
||||
else
|
||||
'app'
|
||||
options.setProxyNotControllable(reason)
|
||||
noRevert = true
|
||||
else
|
||||
options.setProxyNotControllable(null)
|
||||
|
||||
if details['levelOfControl'] == 'controlled_by_this_extension'
|
||||
internal = true
|
||||
return if not notControllableBefore
|
||||
Log.log('external proxy: ', details)
|
||||
|
||||
# Chromium will send chrome.proxy.settings.onChange on extension unload,
|
||||
# just after the current extension has lost control of the proxy settings.
|
||||
# This is just annoying, and may change the currentProfileName state
|
||||
# suprisingly.
|
||||
# To workaround this issue, wait for some time before setting the proxy.
|
||||
# However this will cause some delay before the settings are processed.
|
||||
clearTimeout(timeout) if timeout?
|
||||
parsed = null
|
||||
timeout = setTimeout (->
|
||||
if parsed
|
||||
options.setExternalProfile(parsed,
|
||||
{noRevert: noRevert, internal: internal})
|
||||
), 500
|
||||
|
||||
parsed = proxyImpl.parseExternalProfile(details, options._options)
|
||||
return
|
||||
|
||||
external = false
|
||||
options.currentProfileChanged = (reason) ->
|
||||
iconCache = {}
|
||||
|
||||
if reason == 'external'
|
||||
external = true
|
||||
else if reason != 'clearBadge'
|
||||
external = false
|
||||
|
||||
current = options.currentProfile()
|
||||
currentName = ''
|
||||
if current
|
||||
currentName = dispName(current.name)
|
||||
if current.profileType == 'VirtualProfile'
|
||||
realCurrentName = current.defaultProfileName
|
||||
currentName += " [#{dispName(realCurrentName)}]"
|
||||
current = options.profile(realCurrentName)
|
||||
|
||||
details = options.printProfile(current)
|
||||
if currentName
|
||||
title = chrome.i18n.getMessage('browserAction_titleWithResult', [
|
||||
currentName, '', details])
|
||||
shortTitle = 'Omega: ' + currentName # TODO: I18n.
|
||||
else
|
||||
title = details
|
||||
shortTitle = 'Omega: ' + details # TODO: I18n.
|
||||
|
||||
if external and current.profileType != 'SystemProfile'
|
||||
message = chrome.i18n.getMessage('browserAction_titleExternalProxy')
|
||||
title = message + '\n' + title
|
||||
shortTitle = 'Omega-Extern: ' + details # TODO: I18n.
|
||||
options.setBadge()
|
||||
|
||||
if not current.name or not OmegaPac.Profiles.isInclusive(current)
|
||||
icon = drawIcon(current.color)
|
||||
else
|
||||
icon = drawIcon(options.profile('direct').color, current.color)
|
||||
|
||||
tabs.resetAll(
|
||||
icon: icon
|
||||
title: title
|
||||
shortTitle: shortTitle
|
||||
)
|
||||
|
||||
encodeError = (obj) ->
|
||||
if obj instanceof Error
|
||||
{
|
||||
_error: 'error'
|
||||
name: obj.name
|
||||
message: obj.message
|
||||
stack: obj.stack
|
||||
original: obj
|
||||
}
|
||||
else
|
||||
obj
|
||||
|
||||
refreshActivePageIfEnabled = ->
|
||||
return if zeroLocalStorage['omega.local.refreshOnProfileChange'] == 'false'
|
||||
chrome.tabs.query {active: true, lastFocusedWindow: true}, (tabs) ->
|
||||
url = tabs[0].pendingUrl or tabs[0].url
|
||||
return if not url
|
||||
return if url.substr(0, 6) == 'chrome'
|
||||
return if url.substr(0, 6) == 'about:'
|
||||
return if url.substr(0, 4) == 'moz-'
|
||||
if tabs[0].pendingUrl
|
||||
chrome.tabs.update(tabs[0].id, {url: url})
|
||||
else
|
||||
chrome.tabs.reload(tabs[0].id, {bypassCache: true})
|
||||
|
||||
|
||||
resetAllOptions = ->
|
||||
options.ready.then ->
|
||||
options._watchStop?()
|
||||
options._syncWatchStop?()
|
||||
Promise.all([
|
||||
chrome.storage.sync.clear(),
|
||||
chrome.storage.local.clear()
|
||||
])
|
||||
chrome.runtime.onMessage.addListener (request, sender, respond) ->
|
||||
return unless request and request.method
|
||||
options.ready.then ->
|
||||
if request.method == 'resetAllOptions'
|
||||
target = globalThis
|
||||
method = resetAllOptions
|
||||
else if request.method == 'getState'
|
||||
target = state
|
||||
method = state.get
|
||||
else if request.method == 'setState'
|
||||
target = state
|
||||
method = state.set
|
||||
else
|
||||
target = options
|
||||
method = target[request.method]
|
||||
if typeof method != 'function'
|
||||
Log.error("No such method #{request.method}!")
|
||||
respond(
|
||||
error:
|
||||
reason: 'noSuchMethod'
|
||||
)
|
||||
options._inspect = new OmegaTargetCurrent.Inspect (url, tab) ->
|
||||
if url == tab.url
|
||||
options.clearBadge()
|
||||
tabs.processTab(tab)
|
||||
state.remove('inspectUrl')
|
||||
return
|
||||
|
||||
promise = Promise.resolve().then -> method.apply(target, request.args)
|
||||
if request.refreshActivePage
|
||||
promise.then refreshActivePageIfEnabled
|
||||
return if request.noReply
|
||||
state.set({inspectUrl: url})
|
||||
|
||||
promise.then (result) ->
|
||||
if request.method == 'updateProfile'
|
||||
for own key, value of result
|
||||
result[key] = encodeError(value)
|
||||
respond(result: result)
|
||||
actionForUrl(url).then (action) ->
|
||||
return if not action
|
||||
parsedUrl = OmegaTargetCurrent.Url.parse(url)
|
||||
if parsedUrl.hostname == OmegaTargetCurrent.Url.parse(tab.url).hostname
|
||||
urlDisp = parsedUrl.path
|
||||
else
|
||||
urlDisp = parsedUrl.hostname
|
||||
|
||||
promise.catch (error) ->
|
||||
Log.error(request.method + ' ==>', error)
|
||||
respond(error: encodeError(error))
|
||||
title = chrome.i18n.getMessage(
|
||||
'browserAction_titleInspect', urlDisp) + '\n'
|
||||
title += action.title
|
||||
chrome.action.setTitle(title: title, tabId: tab.id)
|
||||
tabs.setTabBadge(tab, {
|
||||
text: '#'
|
||||
color: action.resultColor
|
||||
})
|
||||
|
||||
# Wait for my response!
|
||||
return true unless request.noReply
|
||||
options.setProxyNotControllable(null)
|
||||
timeout = null
|
||||
|
||||
proxyImpl.watchProxyChange (details) ->
|
||||
return if options.externalApi.disabled
|
||||
return unless details
|
||||
notControllableBefore = options.proxyNotControllable()
|
||||
internal = false
|
||||
noRevert = false
|
||||
switch details['levelOfControl']
|
||||
when "controlled_by_other_extensions", "not_controllable"
|
||||
reason =
|
||||
if details['levelOfControl'] == 'not_controllable'
|
||||
'policy'
|
||||
else
|
||||
'app'
|
||||
options.setProxyNotControllable(reason)
|
||||
noRevert = true
|
||||
else
|
||||
options.setProxyNotControllable(null)
|
||||
|
||||
if details['levelOfControl'] == 'controlled_by_this_extension'
|
||||
internal = true
|
||||
return if not notControllableBefore
|
||||
Log.log('external proxy: ', details)
|
||||
|
||||
# Chromium will send chrome.proxy.settings.onChange on extension unload,
|
||||
# just after the current extension has lost control of the proxy settings.
|
||||
# This is just annoying, and may change the currentProfileName state
|
||||
# suprisingly.
|
||||
# To workaround this issue, wait for some time before setting the proxy.
|
||||
# However this will cause some delay before the settings are processed.
|
||||
clearTimeout(timeout) if timeout?
|
||||
parsed = null
|
||||
timeout = setTimeout (->
|
||||
if parsed
|
||||
options.setExternalProfile(parsed,
|
||||
{noRevert: noRevert, internal: internal})
|
||||
), 500
|
||||
|
||||
parsed = proxyImpl.parseExternalProfile(details, options._options)
|
||||
return
|
||||
|
||||
external = false
|
||||
options.currentProfileChanged = (reason) ->
|
||||
iconCache = {}
|
||||
|
||||
if reason == 'external'
|
||||
external = true
|
||||
else if reason != 'clearBadge'
|
||||
external = false
|
||||
|
||||
current = options.currentProfile()
|
||||
currentName = ''
|
||||
if current
|
||||
currentName = dispName(current.name)
|
||||
if current.profileType == 'VirtualProfile'
|
||||
realCurrentName = current.defaultProfileName
|
||||
currentName += " [#{dispName(realCurrentName)}]"
|
||||
current = options.profile(realCurrentName)
|
||||
|
||||
details = options.printProfile(current)
|
||||
if currentName
|
||||
title = chrome.i18n.getMessage('browserAction_titleWithResult', [
|
||||
currentName, '', details])
|
||||
shortTitle = 'Omega: ' + currentName # TODO: I18n.
|
||||
else
|
||||
title = details
|
||||
shortTitle = 'Omega: ' + details # TODO: I18n.
|
||||
|
||||
if external and current.profileType != 'SystemProfile'
|
||||
message = chrome.i18n.getMessage('browserAction_titleExternalProxy')
|
||||
title = message + '\n' + title
|
||||
shortTitle = 'Omega-Extern: ' + details # TODO: I18n.
|
||||
options.setBadge()
|
||||
|
||||
if not current.name or not OmegaPac.Profiles.isInclusive(current)
|
||||
icon = drawIcon(current.color)
|
||||
else
|
||||
icon = drawIcon(options.profile('direct').color, current.color)
|
||||
|
||||
tabs.resetAll(
|
||||
icon: icon
|
||||
title: title
|
||||
shortTitle: shortTitle
|
||||
)
|
||||
|
||||
encodeError = (obj) ->
|
||||
if obj instanceof Error
|
||||
{
|
||||
_error: 'error'
|
||||
name: obj.name
|
||||
message: obj.message
|
||||
stack: obj.stack
|
||||
original: obj
|
||||
}
|
||||
else
|
||||
obj
|
||||
|
||||
refreshActivePageIfEnabled = ->
|
||||
return if zeroStorage['omega.local.refreshOnProfileChange'] == 'false'
|
||||
chrome.tabs.query {active: true, lastFocusedWindow: true}, (tabs) ->
|
||||
url = tabs[0].pendingUrl or tabs[0].url
|
||||
return if not url
|
||||
return if url.substr(0, 6) == 'chrome'
|
||||
return if url.substr(0, 6) == 'about:'
|
||||
return if url.substr(0, 4) == 'moz-'
|
||||
if tabs[0].pendingUrl
|
||||
chrome.tabs.update(tabs[0].id, {url: url})
|
||||
else
|
||||
chrome.tabs.reload(tabs[0].id, {bypassCache: true})
|
||||
|
||||
|
||||
resetAllOptions = ->
|
||||
options.ready.then ->
|
||||
options._watchStop?()
|
||||
options._syncWatchStop?()
|
||||
Promise.all([
|
||||
chrome.storage.sync.clear(),
|
||||
chrome.storage.local.clear()
|
||||
])
|
||||
chrome.runtime.onMessage.addListener (request, sender, respond) ->
|
||||
return unless request and request.method
|
||||
options.ready.then ->
|
||||
if request.method == 'resetAllOptions'
|
||||
target = globalThis
|
||||
method = resetAllOptions
|
||||
else if request.method == 'getState'
|
||||
target = state
|
||||
method = state.get
|
||||
else if request.method == 'setState'
|
||||
target = state
|
||||
method = state.set
|
||||
else
|
||||
target = options
|
||||
method = target[request.method]
|
||||
if typeof method != 'function'
|
||||
Log.error("No such method #{request.method}!")
|
||||
respond(
|
||||
error:
|
||||
reason: 'noSuchMethod'
|
||||
)
|
||||
return
|
||||
|
||||
promise = Promise.resolve().then -> method.apply(target, request.args)
|
||||
if request.refreshActivePage
|
||||
promise.then refreshActivePageIfEnabled
|
||||
return if request.noReply
|
||||
|
||||
promise.then (result) ->
|
||||
if request.method == 'updateProfile'
|
||||
for own key, value of result
|
||||
result[key] = encodeError(value)
|
||||
respond(result: result)
|
||||
|
||||
promise.catch (error) ->
|
||||
Log.error(request.method + ' ==>', error)
|
||||
respond(error: encodeError(error))
|
||||
|
||||
# Wait for my response!
|
||||
return true unless request.noReply
|
||||
globalThis.zeroBackground = zeroBackground
|
||||
|
@ -262,32 +262,8 @@ class ChromeOptions extends OmegaTarget.Options
|
||||
chrome.i18n.getMessage('browserAction_profileDetails_' + type) || null
|
||||
|
||||
upgrade: (options, changes) ->
|
||||
super(options).catch (err) =>
|
||||
return Promise.reject err if options?['schemaVersion']
|
||||
getOldOptions = Promise.reject()
|
||||
|
||||
getOldOptions = getOldOptions.catch ->
|
||||
if options?['config']
|
||||
Promise.resolve options
|
||||
else if localStorage['config']
|
||||
Promise.resolve localStorage
|
||||
else
|
||||
Promise.reject new OmegaTarget.Options.NoOptionsError()
|
||||
|
||||
getOldOptions.then (oldOptions) =>
|
||||
i18n = {
|
||||
upgrade_profile_auto: chrome.i18n.getMessage('upgrade_profile_auto')
|
||||
}
|
||||
try
|
||||
# Upgrade from SwitchySharp.
|
||||
upgraded = require('./upgrade')(oldOptions, i18n)
|
||||
catch ex
|
||||
@log.error(ex)
|
||||
return Promise.reject ex
|
||||
if localStorage['config']
|
||||
Object.getPrototypeOf(localStorage).clear.call(localStorage)
|
||||
@_state.set({'firstRun': 'upgrade'})
|
||||
return this && super(upgraded, upgraded)
|
||||
super(options).catch (err) ->
|
||||
return Promise.reject err
|
||||
|
||||
onFirstRun: (reason) ->
|
||||
console.log('first run ....')
|
||||
|
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) ->
|
||||
console.log('globalThis onerror', arguments)
|
||||
return unless globalThis.localStorage
|
||||
log = localStorage['log'] || ''
|
||||
if err?.stack
|
||||
log += err.stack + '\n\n'
|
||||
|
Loading…
Reference in New Issue
Block a user