indexeddbshim
Version:
A polyfill for IndexedDB using WebSql
1,468 lines (1,393 loc) • 66.8 kB
JavaScript
/*! indexeddbshim - v16.0.0 - 6/14/2025 */
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.IDBKeyUtils = {}));
})(this, (function (exports) { 'use strict';
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithHoles(r) {
if (Array.isArray(r)) return r;
}
function _arrayWithoutHoles(r) {
if (Array.isArray(r)) return _arrayLikeToArray(r);
}
function _createForOfIteratorHelper(r, e) {
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (!t) {
if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) {
t && (r = t);
var n = 0,
F = function () {};
return {
s: F,
n: function () {
return n >= r.length ? {
done: true
} : {
done: false,
value: r[n++]
};
},
e: function (r) {
throw r;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var o,
a = true,
u = false;
return {
s: function () {
t = t.call(r);
},
n: function () {
var r = t.next();
return a = r.done, r;
},
e: function (r) {
u = true, o = r;
},
f: function () {
try {
a || null == t.return || t.return();
} finally {
if (u) throw o;
}
}
};
}
function _iterableToArray(r) {
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
}
function _iterableToArrayLimit(r, l) {
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (null != t) {
var e,
n,
i,
u,
a = [],
f = true,
o = false;
try {
if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
} catch (r) {
o = true, n = r;
} finally {
try {
if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return;
} finally {
if (o) throw n;
}
}
return a;
}
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _slicedToArray(r, e) {
return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest();
}
function _toConsumableArray(r) {
return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
/* eslint-disable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/147 */
/**
* @typedef {T[keyof T]} ValueOf<T>
* @template T
*/
/* eslint-enable jsdoc/valid-types -- https://github.com/jsdoc-type-pratt-parser/jsdoc-type-pratt-parser/issues/147 */
/**
* @typedef {{unlink: (path: string, cb: import('fs').NoParamCallback) => void}} FSApi
*/
/**
* @typedef {{
* DEBUG: boolean,
* cacheDatabaseInstances: boolean,
* autoName: boolean,
* fullIDLSupport: boolean,
* checkOrigin: boolean,
* cursorPreloadPackSize: number,
* UnicodeIDStart: string,
* UnicodeIDContinue: string,
* registerSCA: (
* preset: import('typeson').Preset
* ) => import('typeson').Preset,
* avoidAutoShim: boolean,
* win: {
* openDatabase: (name: string, version: string, displayName: string, estimatedSize: number) => import('websql-configurable').default
* },
* DEFAULT_DB_SIZE: number,
* useSQLiteIndexes: boolean,
* fs: FSApi,
* addNonIDBGlobals: boolean,
* replaceNonIDBGlobals: boolean,
* escapeDatabaseName: (name: string) => string,
* unescapeDatabaseName: (name: string) => string,
* databaseCharacterEscapeList: string|false,
* databaseNameLengthLimit: number|false,
* escapeNFDForDatabaseNames: boolean,
* addSQLiteExtension: boolean,
* memoryDatabase: string,
* deleteDatabaseFiles: boolean,
* databaseBasePath: string,
* sysDatabaseBasePath: string,
* sqlBusyTimeout: number,
* sqlTrace: () => void,
* sqlProfile: () => void,
* createIndexes: boolean
* }} ConfigValues
*/
/**
* @typedef {ValueOf<ConfigValues>} ConfigValue
*/
/** @type {{[key: string]: ConfigValue}} */
var map = {};
var CFG = /** @type {ConfigValues} */{};
/**
* @typedef {keyof ConfigValues} KeyofConfigValues
*/
/**
* @typedef {KeyofConfigValues[]} Config
*/
/** @type {Config} */
[
// Boolean for verbose reporting
'DEBUG',
// Effectively defaults to false (ignored unless `true`)
// Boolean (effectively defaults to true) on whether to cache WebSQL
// `openDatabase` instances
'cacheDatabaseInstances',
// Boolean on whether to auto-name databases (based on an
// auto-increment) when the empty string is supplied; useful with
// `memoryDatabase`; defaults to `false` which means the empty string
// will be used as the (valid) database name
'autoName',
// Determines whether the slow-performing `Object.setPrototypeOf`
// calls required for full WebIDL compliance will be used. Probably
// only needed for testing or environments where full introspection
// on class relationships is required; see
// http://stackoverflow.com/questions/41927589/rationales-consequences-of-webidl-class-inheritance-requirements
'fullIDLSupport',
// Effectively defaults to false (ignored unless `true`)
// Boolean on whether to perform origin checks in `IDBFactory` methods
// Effectively defaults to `true` (must be set to `false` to cancel checks)
'checkOrigin',
// Used by `IDBCursor` continue methods for number of records to cache;
// Defaults to 100
'cursorPreloadPackSize',
// See optional API (`shimIndexedDB.__setUnicodeIdentifiers`);
// or just use the Unicode builds which invoke this method
// automatically using the large, fully spec-compliant, regular
// expression strings of `src/UnicodeIdentifiers.js`)
// In the non-Unicode builds, defaults to /[$A-Z_a-z]/
'UnicodeIDStart',
// In the non-Unicode builds, defaults to /[$0-9A-Z_a-z]/
'UnicodeIDContinue',
// Used by SCA.js for optional restructuring of typeson-registry
// Structured Cloning Algorithm; should only be needed for ensuring data
// created in 3.* versions of IndexedDBShim continue to work; see the
// library `typeson-registry-sca-reverter` to get a function to do this
'registerSCA',
// BROWSER-SPECIFIC CONFIG
'avoidAutoShim',
// Where WebSQL is detected but where `indexedDB` is
// missing or poor support is known (non-Chrome Android or
// non-Safari iOS9), the shim will be auto-applied without
// `shimIndexedDB.__useShim()`. Set this to `true` to avoid forcing
// the shim for such cases.
// -----------SQL CONFIG----------
// Object (`window` in the browser) on which there may be an
// `openDatabase` method (if any) for WebSQL. (The browser
// throws if attempting to call `openDatabase` without the window
// so this is why the config doesn't just allow the function.)
// Defaults to `window` or `self` in browser builds or
// a singleton object with the `openDatabase` method set to
// the "websql" package in Node.
'win',
// For internal `openDatabase` calls made by `IDBFactory` methods;
// per the WebSQL spec, "User agents are expected to use the display name
// and the estimated database size to optimize the user experience.
// For example, a user agent could use the estimated size to suggest an
// initial quota to the user. This allows a site that is aware that it
// will try to use hundreds of megabytes to declare this upfront, instead
// of the user agent prompting the user for permission to increase the
// quota every five megabytes."
// Defaults to (4 * 1024 * 1024) or (25 * 1024 * 1024) in Safari
'DEFAULT_DB_SIZE',
// Whether to create indexes on SQLite tables (and also whether to try
// dropping)
// Effectively defaults to `false` (ignored unless `true`)
'useSQLiteIndexes',
// NODE-IMPINGING SETTINGS (created for sake of limitations in Node
// or desktop file system implementation but applied by default in
// browser for parity)
// File system module with `unlink` to remove deleted database files
'fs',
// Used when setting global shims to determine whether to try to add
// other globals shimmed by the library (`ShimDOMException`,
// `ShimDOMStringList`, `ShimEvent`, `ShimCustomEvent`, `ShimEventTarget`)
// Effectively defaults to `false` (ignored unless `true`)
'addNonIDBGlobals',
// Used when setting global shims to determine whether to try to overwrite
// other globals shimmed by the library (`DOMException`, `DOMStringList`,
// `Event`, `CustomEvent`, `EventTarget`)
// Effectively defaults to `false` (ignored unless `true`)
'replaceNonIDBGlobals',
// Overcoming limitations with node-sqlite3/storing database name on
// file systems
// https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
// Defaults to prefixing database with `D_`, escaping
// `databaseCharacterEscapeList`, escaping NUL, and
// escaping upper case letters, as well as enforcing
// `databaseNameLengthLimit`
'escapeDatabaseName',
// Not used internally; usable as a convenience method
'unescapeDatabaseName',
// Defaults to global regex representing the following
// (characters nevertheless commonly reserved in modern,
// Unicode-supporting systems): 0x00-0x1F 0x7F " * / : < > ? \ |
'databaseCharacterEscapeList',
// Defaults to 254 (shortest typical modern file length limit)
'databaseNameLengthLimit',
// Boolean defaulting to true on whether to escape NFD-escaping
// characters to avoid clashes on MacOS which performs NFD on files
'escapeNFDForDatabaseNames',
// Boolean on whether to add the `.sqlite` extension to file names;
// defaults to `true`
'addSQLiteExtension',
// Various types of in-memory databases that can auto-delete
['memoryDatabase',
/**
* @param {string} val
* @throws {TypeError}
* @returns {void}
*/
function (val) {
if (!/^(?::memory:|file::memory:(\?(?:[\0-"\$-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?(#(?:[\0-\t\x0B\f\x0E-\u2027\u202A-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])*)?)?$/.test(/** @type {string} */val)) {
throw new TypeError('`memoryDatabase` must be the empty string, ":memory:", or a ' + '"file::memory:[?queryString][#hash] URL".');
}
}],
// NODE-SPECIFIC CONFIG
// Boolean on whether to delete the database file itself after
// `deleteDatabase`; defaults to `true` as the database will be empty
'deleteDatabaseFiles', 'databaseBasePath', 'sysDatabaseBasePath',
// NODE-SPECIFIC WEBSQL CONFIG
'sqlBusyTimeout',
// Defaults to 1000
'sqlTrace',
// Callback not used by default
'sqlProfile',
// Callback not used by default
'createIndexes'].forEach(function (prop) {
/** @type {(val: any) => void} */
var validator;
if (Array.isArray(prop)) {
var _prop = prop;
var _prop2 = _slicedToArray(_prop, 2);
prop = _prop2[0];
validator = _prop2[1];
}
Object.defineProperty(CFG, prop, {
get: function get() {
return map[prop];
},
set: function set(val) {
if (validator) {
validator(val);
}
map[prop] = val;
}
});
});
/**
* Creates a native DOMException, for browsers that support it.
* @param {string} name
* @param {string} message
* @returns {DOMException}
*/
function createNativeDOMException(name, message) {
// @ts-expect-error It's ok
return new DOMException.prototype.constructor(message, name || 'DOMException');
}
// From web-platform-tests testharness.js name_code_map (though not in new spec)
/**
* @typedef {"IndexSizeError"|"HierarchyRequestError"|"WrongDocumentError"|
* "InvalidCharacterError"|"NoModificationAllowedError"|"NotFoundError"|
* "NotSupportedError"|"InUseAttributeError"|"InvalidStateError"|
* "SyntaxError"|"InvalidModificationError"|"NamespaceError"|
* "InvalidAccessError"|"TypeMismatchError"|"SecurityError"|
* "NetworkError"|"AbortError"|"URLMismatchError"|"QuotaExceededError"|
* "TimeoutError"|"InvalidNodeTypeError"|"DataCloneError"|"EncodingError"|
* "NotReadableError"|"UnknownError"|"ConstraintError"|"DataError"|
* "TransactionInactiveError"|"ReadOnlyError"|"VersionError"|
* "OperationError"|"NotAllowedError"} Code
*/
var codes = {
IndexSizeError: 1,
HierarchyRequestError: 3,
WrongDocumentError: 4,
InvalidCharacterError: 5,
NoModificationAllowedError: 7,
NotFoundError: 8,
NotSupportedError: 9,
InUseAttributeError: 10,
InvalidStateError: 11,
SyntaxError: 12,
InvalidModificationError: 13,
NamespaceError: 14,
InvalidAccessError: 15,
TypeMismatchError: 17,
SecurityError: 18,
NetworkError: 19,
AbortError: 20,
URLMismatchError: 21,
QuotaExceededError: 22,
TimeoutError: 23,
InvalidNodeTypeError: 24,
DataCloneError: 25,
EncodingError: 0,
NotReadableError: 0,
UnknownError: 0,
ConstraintError: 0,
DataError: 0,
TransactionInactiveError: 0,
ReadOnlyError: 0,
VersionError: 0,
OperationError: 0,
NotAllowedError: 0
};
/**
* @typedef {"INDEX_SIZE_ERR"|"DOMSTRING_SIZE_ERR"|"HIERARCHY_REQUEST_ERR"|
* "WRONG_DOCUMENT_ERR"|"INVALID_CHARACTER_ERR"|"NO_DATA_ALLOWED_ERR"|
* "NO_MODIFICATION_ALLOWED_ERR"|"NOT_FOUND_ERR"|"NOT_SUPPORTED_ERR"|
* "INUSE_ATTRIBUTE_ERR"|"INVALID_STATE_ERR"|"SYNTAX_ERR"|
* "INVALID_MODIFICATION_ERR"|"NAMESPACE_ERR"|"INVALID_ACCESS_ERR"|
* "VALIDATION_ERR"|"TYPE_MISMATCH_ERR"|"SECURITY_ERR"|"NETWORK_ERR"|
* "ABORT_ERR"|"URL_MISMATCH_ERR"|"QUOTA_EXCEEDED_ERR"|"TIMEOUT_ERR"|
* "INVALID_NODE_TYPE_ERR"|"DATA_CLONE_ERR"} LegacyCode
*/
var legacyCodes = {
INDEX_SIZE_ERR: 1,
DOMSTRING_SIZE_ERR: 2,
HIERARCHY_REQUEST_ERR: 3,
WRONG_DOCUMENT_ERR: 4,
INVALID_CHARACTER_ERR: 5,
NO_DATA_ALLOWED_ERR: 6,
NO_MODIFICATION_ALLOWED_ERR: 7,
NOT_FOUND_ERR: 8,
NOT_SUPPORTED_ERR: 9,
INUSE_ATTRIBUTE_ERR: 10,
INVALID_STATE_ERR: 11,
SYNTAX_ERR: 12,
INVALID_MODIFICATION_ERR: 13,
NAMESPACE_ERR: 14,
INVALID_ACCESS_ERR: 15,
VALIDATION_ERR: 16,
TYPE_MISMATCH_ERR: 17,
SECURITY_ERR: 18,
NETWORK_ERR: 19,
ABORT_ERR: 20,
URL_MISMATCH_ERR: 21,
QUOTA_EXCEEDED_ERR: 22,
TIMEOUT_ERR: 23,
INVALID_NODE_TYPE_ERR: 24,
DATA_CLONE_ERR: 25
};
/**
*
* @returns {typeof DOMException}
*/
function createNonNativeDOMExceptionClass() {
/**
* @param {string|undefined} message
* @param {Code|LegacyCode} name
* @returns {void}
*/
function DOMException(message, name) {
// const err = Error.prototype.constructor.call(this, message); // Any use to this? Won't set this.message
this[Symbol.toStringTag] = 'DOMException';
this._code = name in codes ? codes[(/** @type {Code} */name)] : legacyCodes[(/** @type {LegacyCode} */name)] || 0;
this._name = name || 'Error';
// We avoid `String()` in this next line as it converts Symbols
this._message = message === undefined ? '' : '' + message; // eslint-disable-line no-implicit-coercion -- Don't convert symbols
Object.defineProperty(this, 'code', {
configurable: true,
enumerable: true,
writable: true,
value: this._code
});
if (name !== undefined) {
Object.defineProperty(this, 'name', {
configurable: true,
enumerable: true,
writable: true,
value: this._name
});
}
if (message !== undefined) {
Object.defineProperty(this, 'message', {
configurable: true,
enumerable: false,
writable: true,
value: this._message
});
}
}
// Necessary for W3C tests which complains if `DOMException` has properties on its "own" prototype
// class DummyDOMException extends Error {}; // Sometimes causing problems in Node
/* eslint-disable func-name-matching -- See above */
/**
* @class
*/
var DummyDOMException = function DOMException() {/* */};
/* eslint-enable func-name-matching -- See above */
DummyDOMException.prototype = Object.create(Error.prototype); // Intended for subclassing
/** @type {const} */
['name', 'message'].forEach(function (prop) {
Object.defineProperty(DummyDOMException.prototype, prop, {
enumerable: true,
/**
* @this {DOMException}
* @returns {string}
*/
get: function get() {
if (!(this instanceof DOMException ||
// @ts-expect-error Just checking
this instanceof DummyDOMException ||
// @ts-expect-error Just checking
this instanceof Error)) {
throw new TypeError('Illegal invocation');
}
return this[prop === 'name' ? '_name' : '_message'];
}
});
});
// DOMException uses the same `toString` as `Error`
Object.defineProperty(DummyDOMException.prototype, 'code', {
configurable: true,
enumerable: true,
get: function get() {
throw new TypeError('Illegal invocation');
}
});
// @ts-expect-error It's ok
DOMException.prototype = new DummyDOMException();
DOMException.prototype[Symbol.toStringTag] = 'DOMExceptionPrototype';
Object.defineProperty(DOMException, 'prototype', {
writable: false
});
var keys = Object.keys(codes);
/** @type {(keyof codes)[]} */
keys.forEach(function (codeName) {
Object.defineProperty(DOMException.prototype, codeName, {
enumerable: true,
configurable: false,
value: codes[codeName]
});
Object.defineProperty(DOMException, codeName, {
enumerable: true,
configurable: false,
value: codes[codeName]
});
});
/** @type {(keyof legacyCodes)[]} */
Object.keys(legacyCodes).forEach(function (codeName) {
Object.defineProperty(DOMException.prototype, codeName, {
enumerable: true,
configurable: false,
value: legacyCodes[codeName]
});
Object.defineProperty(DOMException, codeName, {
enumerable: true,
configurable: false,
value: legacyCodes[codeName]
});
});
Object.defineProperty(DOMException.prototype, 'constructor', {
writable: true,
configurable: true,
enumerable: false,
value: DOMException
});
// @ts-expect-error We don't need all its properties
return DOMException;
}
var ShimNonNativeDOMException = createNonNativeDOMExceptionClass();
/**
* Creates a generic Error object.
* @param {string} name
* @param {string} message
* @returns {Error}
*/
function createNonNativeDOMException(name, message) {
return new ShimNonNativeDOMException(message, name);
}
/**
* @typedef {{
* message: string|DOMString
* }} ErrorLike
*/
/**
* Logs detailed error information to the console.
* @param {string} name
* @param {string} message
* @param {string|ErrorLike|boolean|null} [error]
* @returns {void}
*/
function logError(name, message, error) {
if (CFG.DEBUG) {
var msg = error && _typeof(error) === 'object' && error.message ? error.message : (/** @type {string} */error);
var method = typeof console.error === 'function' ? 'error' : 'log';
console[method](name + ': ' + message + '. ' + (msg || ''));
if (console.trace) {
console.trace();
}
}
}
/**
* @typedef {any} ArbitraryValue
*/
/**
* @param {ArbitraryValue} obj
* @returns {boolean}
*/
function isErrorOrDOMErrorOrDOMException(obj) {
return obj && _typeof(obj) === 'object' &&
// We don't use util.isObj here as mutual dependency causing problems in Babel with browser
typeof obj.name === 'string';
}
var test,
useNativeDOMException = false;
// Test whether we can use the browser's native DOMException class
try {
test = createNativeDOMException('test name', 'test message');
if (isErrorOrDOMErrorOrDOMException(test) && test.name === 'test name' && test.message === 'test message') {
// Native DOMException works as expected
useNativeDOMException = true;
}
} catch (err) {}
var createDOMException = useNativeDOMException
// eslint-disable-next-line @stylistic/operator-linebreak -- Need JSDoc
?
/**
* @param {string} name
* @param {string} message
* @param {ErrorLike} [error]
* @returns {DOMException}
*/
function (name, message, error) {
logError(name, message, error);
return createNativeDOMException(name, message);
}
// eslint-disable-next-line @stylistic/operator-linebreak -- Need JSDoc
:
/**
* @param {string} name
* @param {string} message
* @param {ErrorLike} [error]
* @returns {Error}
*/
function (name, message, error) {
logError(name, message, error);
return createNonNativeDOMException(name, message);
};
/**
* @typedef {number} Integer
*/
/**
* @param {string} arg
* @returns {string}
*/
function escapeUnmatchedSurrogates(arg) {
// http://stackoverflow.com/a/6701665/271577
return arg.replaceAll(/((?:[\uD800-\uDBFF](?![\uDC00-\uDFFF])))(?!(?:(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))|(^|(?:[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))((?:(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))/g, function (_, unmatchedHighSurrogate, precedingLow, unmatchedLowSurrogate) {
// Could add a corresponding surrogate for compatibility with `node-sqlite3`: http://bugs.python.org/issue12569 and http://stackoverflow.com/a/6701665/271577
// but Chrome having problems
if (unmatchedHighSurrogate) {
return '^2' + unmatchedHighSurrogate.codePointAt().toString(16).padStart(4, '0');
}
return (precedingLow || '') + '^3' + unmatchedLowSurrogate.codePointAt().toString(16).padStart(4, '0');
});
}
/**
* The escaping of unmatched surrogates was needed by Chrome but not Node.
* @param {string} arg
* @returns {string}
*/
function escapeSQLiteStatement(arg) {
return escapeUnmatchedSurrogates(arg.replaceAll('^', '^^').replaceAll('\0', '^0'));
}
/**
*
* @param {AnyValue} obj
* @returns {obj is object}
*/
function isObj(obj) {
return obj !== null && _typeof(obj) === 'object';
}
/**
*
* @param {object} obj
* @returns {boolean}
*/
function isDate(obj) {
return isObj(obj) && 'getDate' in obj && typeof obj.getDate === 'function';
}
/**
*
* @param {object} obj
* @returns {boolean}
*/
function isBlob(obj) {
return isObj(obj) && 'size' in obj && typeof obj.size === 'number' && 'slice' in obj && typeof obj.slice === 'function' && !('lastModified' in obj);
}
/**
*
* @param {object} obj
* @returns {boolean}
*/
function isFile(obj) {
return isObj(obj) && 'name' in obj && typeof obj.name === 'string' && 'slice' in obj && typeof obj.slice === 'function' && 'lastModified' in obj;
}
/**
*
* @param {AnyValue} obj
* @returns {boolean}
*/
function isBinary(obj) {
return isObj(obj) && 'byteLength' in obj && typeof obj.byteLength === 'number' && ('slice' in obj && typeof obj.slice === 'function' ||
// `TypedArray` (view on buffer) or `ArrayBuffer`
'getFloat64' in obj && typeof obj.getFloat64 === 'function' // `DataView` (view on buffer)
);
}
/**
* @param {AnyValue} v
* @returns {v is null|undefined}
*/
function isNullish(v) {
return v === null || v === undefined;
}
/**
* Compares two keys.
* @param {import('./Key.js').Key} first
* @param {import('./Key.js').Key} second
* @returns {0|1|-1}
*/
function cmp(first, second) {
var encodedKey1 = /** @type {string} */_encode(first);
var encodedKey2 = /** @type {string} */_encode(second);
var result = encodedKey1 > encodedKey2 ? 1 : encodedKey1 === encodedKey2 ? 0 : -1;
if (CFG.DEBUG) {
// verify that the keys encoded correctly
var decodedKey1 = _decode(encodedKey1);
var decodedKey2 = _decode(encodedKey2);
if (_typeof(first) === 'object') {
first = JSON.stringify(first);
decodedKey1 = JSON.stringify(decodedKey1);
}
if (_typeof(second) === 'object') {
second = JSON.stringify(second);
decodedKey2 = JSON.stringify(decodedKey2);
}
// Encoding/decoding mismatches are usually due to a loss of
// floating-point precision
if (decodedKey1 !== first) {
console.warn(first + ' was incorrectly encoded as ' + decodedKey1);
}
if (decodedKey2 !== second) {
console.warn(second + ' was incorrectly encoded as ' + decodedKey2);
}
}
return result;
}
/**
* @typedef {NodeJS.TypedArray|DataView} ArrayBufferView
*/
/**
* @typedef {ArrayBufferView|ArrayBuffer} BufferSource
*/
/**
* @typedef {"number"|"date"|"string"|"binary"|"array"} KeyType
*/
/**
* @typedef {any} Value
*/
/**
* @typedef {any} Key
* @todo Specify possible value more precisely
*/
/**
* @typedef {KeyPath[]} KeyPathArray
*/
/**
* @typedef {string|KeyPathArray} KeyPath
*/
/**
* @typedef {object} KeyValueObject
* @property {KeyType|"NaN"|"null"|"undefined"|"boolean"|"object"|"symbol"|
* "function"|"bigint"} type If not `KeyType`, indicates invalid value
* @property {Value} [value]
* @property {boolean} [invalid]
* @property {string} [message]
* @todo Specify acceptable `value` more precisely
*/
/**
* @typedef {number|string|Date|ArrayBuffer} ValueTypePrimitive
*/
/**
* @typedef {ValueType[]} ValueTypeArray
*/
/**
* @typedef {ValueTypePrimitive|ValueTypeArray} ValueType
*/
/**
* Encodes the keys based on their types. This is required to maintain collations
* We leave space for future keys.
* @type {{[key: string]: Integer|string}}
*/
var keyTypeToEncodedChar = {
invalid: 100,
number: 200,
date: 300,
string: 400,
binary: 500,
array: 600
};
var keyTypes = /** @type {(KeyType|"invalid")[]} */Object.keys(keyTypeToEncodedChar);
keyTypes.forEach(function (k) {
keyTypeToEncodedChar[k] = String.fromCodePoint(/** @type {number} */keyTypeToEncodedChar[k]);
});
var encodedCharToKeyType = keyTypes.reduce(function (o, k) {
o[keyTypeToEncodedChar[k]] = k;
return o;
}, /** @type {{[key: string]: KeyType|"invalid"}} */{});
/**
* The sign values for numbers, ordered from least to greatest.
* - "negativeInfinity": Sorts below all other values.
* - "bigNegative": Negative values less than or equal to negative one.
* - "smallNegative": Negative values between negative one and zero, noninclusive.
* - "smallPositive": Positive values between zero and one, including zero but not one.
* - "largePositive": Positive values greater than or equal to one.
* - "positiveInfinity": Sorts above all other values.
*/
var signValues = ['negativeInfinity', 'bigNegative', 'smallNegative', 'smallPositive', 'bigPositive', 'positiveInfinity'];
/**
* @typedef {any} AnyValue
*/
/**
* @type {{
* [key: string]: {
* encode: (param: any, inArray?: boolean) => string,
* decode: (param: string, inArray?: boolean) => any
* }
* }}
*/
var types = {
invalid: {
/**
* @returns {string}
*/
encode: function encode() {
return keyTypeToEncodedChar.invalid + '-';
},
/**
* @returns {undefined}
*/
decode: function decode() {
return undefined;
}
},
// Numbers are represented in a lexically sortable base-32 sign-exponent-mantissa
// notation.
//
// sign: takes a value between zero and five, inclusive. Represents infinite cases
// and the signs of both the exponent and the fractional part of the number.
// exponent: padded to two base-32 digits, represented by the 32's compliment in the
// "smallPositive" and "bigNegative" cases to ensure proper lexical sorting.
// mantissa: also called the fractional part. Normed 11-digit base-32 representation.
// Represented by the 32's compliment in the "smallNegative" and "bigNegative"
// cases to ensure proper lexical sorting.
number: {
// The encode step checks for six numeric cases and generates 14-digit encoded
// sign-exponent-mantissa strings.
/**
* @param {number} key
* @returns {string}
*/
encode: function encode(key) {
var key32 = key === Number.MIN_VALUE
// Mocha test `IDBFactory/cmp-spec.js` exposed problem for some
// Node (and Chrome) versions with `Number.MIN_VALUE` being treated
// as 0
// https://stackoverflow.com/questions/43305403/number-min-value-and-tostring
? '0.' + '0'.repeat(214) + '2' : Math.abs(key).toString(32);
// Get the index of the decimal.
var decimalIndex = key32.indexOf('.');
// Remove the decimal.
key32 = decimalIndex !== -1 ? key32.replace('.', '') : key32;
// Get the index of the first significant digit.
var significantDigitIndex = key32.search(/(?:[\0-\/1-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/);
// Truncate leading zeros.
key32 = key32.slice(significantDigitIndex);
var sign, exponent, mantissa;
// Finite cases:
if (Number.isFinite(Number(key))) {
// Negative cases:
if (key < 0) {
// Negative exponent case:
if (key > -1) {
sign = signValues.indexOf('smallNegative');
exponent = padBase32Exponent(significantDigitIndex);
mantissa = flipBase32(padBase32Mantissa(key32));
// Non-negative exponent case:
} else {
sign = signValues.indexOf('bigNegative');
exponent = flipBase32(padBase32Exponent(decimalIndex !== -1 ? decimalIndex : key32.length));
mantissa = flipBase32(padBase32Mantissa(key32));
}
// Non-negative cases:
// Negative exponent case:
} else if (key < 1) {
sign = signValues.indexOf('smallPositive');
exponent = flipBase32(padBase32Exponent(significantDigitIndex));
mantissa = padBase32Mantissa(key32);
// Non-negative exponent case:
} else {
sign = signValues.indexOf('bigPositive');
exponent = padBase32Exponent(decimalIndex !== -1 ? decimalIndex : key32.length);
mantissa = padBase32Mantissa(key32);
}
// Infinite cases:
} else {
exponent = zeros(2);
mantissa = zeros(11);
sign = signValues.indexOf(key > 0 ? 'positiveInfinity' : 'negativeInfinity');
}
return keyTypeToEncodedChar.number + '-' + sign + exponent + mantissa;
},
// The decode step must interpret the sign, reflip values encoded as the 32's complements,
// apply signs to the exponent and mantissa, do the base-32 power operation, and return
// the original JavaScript number values.
/**
* @param {string} key
* @returns {number}
*/
decode: function decode(key) {
var sign = Number(key.slice(2, 3));
var exponent = key.slice(3, 5);
var mantissa = key.slice(5, 16);
switch (signValues[sign]) {
case 'negativeInfinity':
return Number.NEGATIVE_INFINITY;
case 'positiveInfinity':
return Number.POSITIVE_INFINITY;
case 'bigPositive':
return pow32(mantissa, exponent);
case 'smallPositive':
exponent = negate(flipBase32(exponent));
return pow32(mantissa, exponent);
case 'smallNegative':
exponent = negate(exponent);
mantissa = flipBase32(mantissa);
return -pow32(mantissa, exponent);
case 'bigNegative':
exponent = flipBase32(exponent);
mantissa = flipBase32(mantissa);
return -pow32(mantissa, exponent);
default:
throw new Error('Invalid number.');
}
}
},
// Strings are encoded as JSON strings (with quotes and unicode characters escaped).
//
// If the strings are in an array, then some extra encoding is done to make sorting work correctly:
// Since we can't force all strings to be the same length, we need to ensure that characters line-up properly
// for sorting, while also accounting for the extra characters that are added when the array itself is encoded as JSON.
// To do this, each character of the string is prepended with a dash ("-"), and a space is added to the end of the string.
// This effectively doubles the size of every string, but it ensures that when two arrays of strings are compared,
// the indexes of each string's characters line up with each other.
string: {
/**
* @param {string} key
* @param {boolean} [inArray]
* @returns {string}
*/
encode: function encode(key, inArray) {
if (inArray) {
// prepend each character with a dash, and append a space to the end
key = key.replaceAll(/((?:[\0-\t\x0B\f\x0E-\u2027\u202A-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))/g, '-$1') + ' ';
}
return keyTypeToEncodedChar.string + '-' + key;
},
/**
* @param {string} key
* @param {boolean} [inArray]
* @returns {string}
*/
decode: function decode(key, inArray) {
key = key.slice(2);
if (inArray) {
// remove the space at the end, and the dash before each character
key = key.slice(0, -1).replaceAll(/-((?:[\0-\t\x0B\f\x0E-\u2027\u202A-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))/g, '$1');
}
return key;
}
},
// Arrays are encoded as JSON strings.
// An extra, value is added to each array during encoding to make
// empty arrays sort correctly.
array: {
/**
* @param {ValueTypeArray} key
* @returns {string}
*/
encode: function encode(key) {
var encoded = [];
var _iterator = _createForOfIteratorHelper(key.entries()),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _step$value = _slicedToArray(_step.value, 2),
i = _step$value[0],
item = _step$value[1];
var encodedItem = _encode(item, true); // encode the array item
encoded[i] = encodedItem;
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
encoded.push(keyTypeToEncodedChar.invalid + '-'); // append an extra item, so empty arrays sort correctly
return keyTypeToEncodedChar.array + '-' + JSON.stringify(encoded);
},
/**
* @param {string} key
* @returns {ValueTypeArray}
*/
decode: function decode(key) {
var decoded = JSON.parse(key.slice(2));
decoded.pop(); // remove the extra item
for (var i = 0; i < decoded.length; i++) {
var item = decoded[i];
var decodedItem = _decode(item, true); // decode the item
decoded[i] = decodedItem;
}
return decoded;
}
},
// Dates are encoded as ISO 8601 strings, in UTC time zone.
date: {
/**
* @param {Date} key
* @returns {string}
*/
encode: function encode(key) {
return keyTypeToEncodedChar.date + '-' + key.toJSON();
},
/**
* @param {string} key
* @returns {Date}
*/
decode: function decode(key) {
return new Date(key.slice(2));
}
},
binary: {
// `ArrayBuffer`/Views on buffers (`TypedArray` or `DataView`)
/**
* @param {BufferSource} key
* @returns {string}
*/
encode: function encode(key) {
return keyTypeToEncodedChar.binary + '-' + (key.byteLength ? _toConsumableArray(getCopyBytesHeldByBufferSource(key)).map(function (b) {
return String(b).padStart(3, '0');
}) // e.g., '255,005,254,000,001,033'
: '');
},
/**
* @param {string} key
* @returns {ArrayBuffer}
*/
decode: function decode(key) {
// Set the entries in buffer's [[ArrayBufferData]] to those in `value`
var k = key.slice(2);
var arr = k.length ? k.split(',').map(function (s) {
return Number.parseInt(s);
}) : [];
var buffer = new ArrayBuffer(arr.length);
var uint8 = new Uint8Array(buffer);
uint8.set(arr);
return buffer;
}
}
};
/**
* Return a padded base-32 exponent value.
* @param {number} n
* @returns {string}
*/
function padBase32Exponent(n) {
var exp = n.toString(32);
return exp.length === 1 ? '0' + exp : exp;
}
/**
* Return a padded base-32 mantissa.
* @param {string} s
* @returns {string}
*/
function padBase32Mantissa(s) {
return (s + zeros(11)).slice(0, 11);
}
/**
* Flips each digit of a base-32 encoded string.
* @param {string} encoded
* @returns {string}
*/
function flipBase32(encoded) {
var flipped = '';
var _iterator2 = _createForOfIteratorHelper(encoded),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var ch = _step2.value;
flipped += (31 - Number.parseInt(ch, 32)).toString(32);
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
return flipped;
}
/**
* Base-32 power function.
* RESEARCH: This function does not precisely decode floats because it performs
* floating point arithmetic to recover values. But can the original values be
* recovered exactly?
* Someone may have already figured out a good way to store JavaScript floats as
* binary strings and convert back. Barring a better method, however, one route
* may be to generate decimal strings that `parseFloat` decodes predictably.
* @param {string} mantissa
* @param {string} exponent
* @returns {number}
*/
function pow32(mantissa, exponent) {
var exp = Number.parseInt(exponent, 32);
if (exp < 0) {
return roundToPrecision(Number.parseInt(mantissa, 32) * Math.pow(32, exp - 10));
}
if (exp < 11) {
var whole = mantissa.slice(0, exp);
var wholeNum = Number.parseInt(whole, 32);
var fraction = mantissa.slice(exp);
var fractionNum = Number.parseInt(fraction, 32) * Math.pow(32, exp - 11);
return roundToPrecision(wholeNum + fractionNum);
}
var expansion = mantissa + zeros(exp - 11);
return Number.parseInt(expansion, 32);
}
/**
* @typedef {number} Float
*/
/**
* @param {Float} num
* @param {Float} [precision]
* @returns {Float}
*/
function roundToPrecision(num) {
var precision = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 16;
return Number.parseFloat(num.toPrecision(precision));
}
/**
* Returns a string of n zeros.
* @param {number} n
* @returns {string}
*/
function zeros(n) {
return '0'.repeat(n);
}
/**
* Negates numeric strings.
* @param {string} s
* @returns {string}
*/
function negate(s) {
return '-' + s;
}
/**
* @param {Key} key
* @returns {KeyType|"invalid"}
*/
function getKeyType(key) {
if (Array.isArray(key)) {
return 'array';
}
if (isDate(key)) {
return 'date';
}
if (isBinary(key)) {
return 'binary';
}
var keyType = _typeof(key);
return ['string', 'number'].includes(keyType) ? (/** @type {"string"|"number"} */keyType) : 'invalid';
}
/**
* Keys must be strings, numbers (besides `NaN`), Dates (if value is not
* `NaN`), binary objects or Arrays.
* @param {Value} input The key input
* @param {Value[]|null|undefined} [seen] An array of already seen keys
* @returns {KeyValueObject}
*/
function convertValueToKey(input, seen) {
return convertValueToKeyValueDecoded(input, seen, false, true);
}
/**
* Currently not in use.
* @param {Value} input
* @returns {KeyValueObject}
*/
function convertValueToMultiEntryKey(input) {
return convertValueToKeyValueDecoded(input, null, true, true);
}
/**
*
* @param {BufferSource} O
* @throws {TypeError}
* @see https://heycam.github.io/webidl/#ref-for-dfn-get-buffer-source-copy-2
* @returns {Uint8Array}
*/
function getCopyBytesHeldByBufferSource(O) {
var offset = 0;
var length = 0;
if (ArrayBuffer.isView(O)) {
// Has [[ViewedArrayBuffer]] internal slot
var arrayBuffer = O.buffer;
if (arrayBuffer === undefined) {
throw new TypeError('Could not copy the bytes held by a buffer source as the buffer was undefined.');
}
offset = O.byteOffset; // [[ByteOffset]] (will also throw as desired if detached)
length = O.byteLength; // [[ByteLength]] (will also throw as desired if detached)
} else {
length = O.byteLength; // [[ArrayBufferByteLength]] on ArrayBuffer (will also throw as desired if detached)
}
// const octets = new Uint8Array(input);
// const octets = types.binary.decode(types.binary.encode(input));
return new Uint8Array(
// Should allow DataView
/** @type {ArrayBuffer} */
'buffer' in O && O.buffer || O, offset, length);
}
/**
* Shortcut utility to avoid returning full keys from `convertValueToKey`
* and subsequent need to process in calling code unless `fullKeys` is
* set; may throw.
* @param {Value} input
* @param {Value[]|null} [seen]
* @param {boolean} [multiEntry]
* @param {boolean} [fullKeys]
* @throws {TypeError} See `getCopyBytesHeldByBufferSource`
* @todo Document other allowable `input`
* @returns {KeyValueObject}
*/
function convertValueToKeyValueDecoded(input, seen, multiEntry, fullKeys) {
seen = seen || [];
if (seen.includes(input)) {
return {
type: 'array',
invalid: true,
message: 'An array key cannot be circular'
};
}
var type = getKeyType(input);
var ret = {
type: type,
value: input
};
switch (type) {
case 'number':
{
if (Number.isNaN(input)) {
// List as 'NaN' type for convenience of consumers in reporting errors
return {
type: 'NaN',
invalid: true
};
}
// https://github.com/w3c/IndexedDB/issues/375
// https://github.com/w3c/IndexedDB/pull/386
if (Object.is(input, -0)) {
return {
type: type,
value: 0
};
}
return /** @type {{type: KeyType; value: Value}} */ret;
}
case 'string':
{
return /** @type {{type: KeyType; value: Value}} */ret;
}
case 'binary':
{
// May throw (if detached)
// Get a copy of the bytes held by the buffer source
// https://heycam.github.io/webidl/#ref-for-dfn-get-buffer-source-copy-2
var octets = getCopyBytesHeldByBufferSource(/** @type {BufferSource} */input);
return {
type: 'binary',
value: octets
};
}
case 'array':
{
// May throw (from binary)
var arr = /** @type {Array<any>} */input;
var len = arr.length;
seen.push(input);
/** @type {(KeyValueObject|Value)[]} */
var keys = [];
var _loop = function _loop() {
// We cannot iterate here with array extras as we must ensure sparse arrays are invalidated
if (!multiEntry && !Object.hasOwn(arr, i)) {
return {
v: {
type: type,
invalid: true,
message: 'Does not have own index property'
}
};
}
try {
var entry = arr[i];
var key = convertValueToKeyValueDecoded(entry, seen, false, fullKeys); // Though steps do not list rethrowing, the next is returnifabrupt when not multiEntry
if (key.invalid) {
if (multiEntry) {
return 0; // continue
}
return {
v: {
type: type,
invalid: true,
message: 'Bad array entry value-to-key conversion'
}
};
}
if (!multiEntry || !fullKeys && keys.every(function (k) {
return cmp(k, key.value) !== 0;
}) || fullKeys && keys.every(function (k) {
return cmp(k, key) !== 0;
})) {
keys.push(fullKeys ? key : key.value);
}
} catch (err) {
if (!multiEntry) {
throw err;
}
}
},
_ret;
for (var i = 0; i < len; i++) {
_ret = _loop();
if (_ret === 0) continue;
if (_ret) return _ret.v;
}
return {
type: type,
value: keys
};
}
case 'date':
{
var date = /** @type {Date} */input;
if (!Number.isNaN(date.getTime())) {
return fullKeys ? {
type: type,
value: date.getTime()
} : {
type: type,
value: new Date(date)
};
}
return {
type: type,
invalid: true,
message: 'Not a valid date'
};
// Falls through
}
case 'invalid':
default:
{
// Other `typeof` types which are not valid keys:
// 'undefined', 'boolean', 'object' (including `null`), 'symbol', 'function'
var _type = input === null ? 'null' : _typeof(input); // Convert `null` for convenience of consumers in reporting errors
return {
type: _type,
invalid: true,
message: 'Not a valid key; type ' + _type
};
}
}
}
/**
*
* @param {Key} key
* @param {boolean} [fullKeys]
* @returns {KeyValueObject}
* @todo Document other allowable `key`?
*/
function convertValueToMultiEntryKeyDecoded(key, fullKeys) {
return convertValueToKeyValueDecoded(key, null, true, fullKeys);
}
/**
* An internal utility.
* @param {Value} input
* @param {Value[]|null|undefined} [seen]
* @throws {DOMException} `DataError`
* @returns {KeyValueObject}
*/
function convertValueToKeyRethrowingAndIfInvalid(input, seen) {
var key = convertValueToKey(input, seen);
if (key.invalid) {
throw createDOMException('DataError', key.message || 'Not a valid key; type: ' + key.type);
}
return key;
}
/**
*
* @param {Value} value
* @param {KeyPath} keyPath
* @param {boolean} multiEntry
* @returns {KeyValueObject|KeyPathEvaluateValue}
* @todo Document other possible return?
*/
function extractKeyFromValueUsingKeyPa