diff --git a/experimental/javascript-wc-indexeddb/.gitignore b/experimental/javascript-wc-indexeddb/.gitignore new file mode 100644 index 000000000..03e05e4c0 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +/node_modules diff --git a/experimental/javascript-wc-indexeddb/README.md b/experimental/javascript-wc-indexeddb/README.md new file mode 100644 index 000000000..57a74c9ba --- /dev/null +++ b/experimental/javascript-wc-indexeddb/README.md @@ -0,0 +1,72 @@ +# Speedometer 3.0: TodoMVC: Web Components IndexedDB + +## Description + +A todoMVC application implemented with native web components and indexedDB as the backing storage. +It utilizes custom elements and html templates to build reusable components. + +In contrast to other workloads, this application uses an updated set of css rules and an optimized dom structure to ensure the application follows best practices in regards to accessibility. + +### Benchmark steps + +In contrast to other versions of the todoMVC workload, this one only shows 10 todo items at a time. + +#### Add 100 items. + +All the items are added to the DOM and to the database, it uses CSS to show only 10 of the items on screen. + +The measured time stops when the last item has been added to the DOM, it doesn't measure the time spent to complete the database update. + +#### Complete 100 items. + +The benchmark runs a loop of 10 iterations. On each iteration 10 items are marked completed (in the DOM and in the database), and the "Next page" button is clicked. When moving to the next page the items in the "current page" are deleted from the DOM. + +The measured time stops when the last item has been marked as completed, it doesn't measure the time spent to complete the database update. + +#### Delete 100 items. + +The benchmarks runs a loop of 10 iterations. On each iteration the 10 items in the current page are deleted (from the DOM and the database), and the "Previous page" button is clicked. + +When moving to the previous page the previous 10 items are loaded from the database, this is included in the measured time. + +## Storage Options + +This application supports two different IndexedDB implementations that can be selected via URL search parameters: + +- **Vanilla IndexedDB** (default): Uses the native IndexedDB API + - Access via: `http://localhost:7005/?storageType=vanilla` or `http://localhost:7005/` +- **Dexie.js**: Uses the Dexie.js wrapper library for IndexedDB + - Access via: `http://localhost:7005/?storageType=dexie` + +Simply use the URL parameters to switch between implementations. The database will be reset when switching between storage types. + +Navigation within the app uses simple hash-based routes like `#/active`, `#/completed`, or `#/` for all todos. + +## Built steps + +A simple build script copies all necessary files to a `dist` folder. +It does not rely on compilers or transpilers and serves raw html, css and js files to the user. + +``` +npm run build +``` + +## Requirements + +The only requirement is an installation of Node, to be able to install dependencies and run scripts to serve a local server. + +``` +* Node (min version: 18.13.0) +* NPM (min version: 8.19.3) +``` + +## Local preview + +``` +terminal: +1. npm install +2. npm run dev + +browser: +1. http://localhost:7005/ +``` diff --git a/experimental/javascript-wc-indexeddb/dist/index.html b/experimental/javascript-wc-indexeddb/dist/index.html new file mode 100644 index 000000000..2a38dbaac --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/index.html @@ -0,0 +1,31 @@ + + + + + + + TodoMVC: JavaScript Web Components + + + + + + + + + + + +
+

todos

+
+ + + + + diff --git a/experimental/javascript-wc-indexeddb/dist/libs/dexie.mjs b/experimental/javascript-wc-indexeddb/dist/libs/dexie.mjs new file mode 100644 index 000000000..f4b4d0c06 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/libs/dexie.mjs @@ -0,0 +1,5873 @@ +/* + * Dexie.js - a minimalistic wrapper for IndexedDB + * =============================================== + * + * By David Fahlander, david.fahlander@gmail.com + * + * Version 4.2.1, Sat Oct 04 2025 + * + * https://dexie.org + * + * Apache License Version 2.0, January 2004, http://www.apache.org/licenses/ + */ + +const _global = typeof globalThis !== 'undefined' ? globalThis : + typeof self !== 'undefined' ? self : + typeof window !== 'undefined' ? window : + global; + +const keys = Object.keys; +const isArray = Array.isArray; +if (typeof Promise !== 'undefined' && !_global.Promise) { + _global.Promise = Promise; +} +function extend(obj, extension) { + if (typeof extension !== 'object') + return obj; + keys(extension).forEach(function (key) { + obj[key] = extension[key]; + }); + return obj; +} +const getProto = Object.getPrototypeOf; +const _hasOwn = {}.hasOwnProperty; +function hasOwn(obj, prop) { + return _hasOwn.call(obj, prop); +} +function props(proto, extension) { + if (typeof extension === 'function') + extension = extension(getProto(proto)); + (typeof Reflect === "undefined" ? keys : Reflect.ownKeys)(extension).forEach(key => { + setProp(proto, key, extension[key]); + }); +} +const defineProperty = Object.defineProperty; +function setProp(obj, prop, functionOrGetSet, options) { + defineProperty(obj, prop, extend(functionOrGetSet && hasOwn(functionOrGetSet, "get") && typeof functionOrGetSet.get === 'function' ? + { get: functionOrGetSet.get, set: functionOrGetSet.set, configurable: true } : + { value: functionOrGetSet, configurable: true, writable: true }, options)); +} +function derive(Child) { + return { + from: function (Parent) { + Child.prototype = Object.create(Parent.prototype); + setProp(Child.prototype, "constructor", Child); + return { + extend: props.bind(null, Child.prototype) + }; + } + }; +} +const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; +function getPropertyDescriptor(obj, prop) { + const pd = getOwnPropertyDescriptor(obj, prop); + let proto; + return pd || (proto = getProto(obj)) && getPropertyDescriptor(proto, prop); +} +const _slice = [].slice; +function slice(args, start, end) { + return _slice.call(args, start, end); +} +function override(origFunc, overridedFactory) { + return overridedFactory(origFunc); +} +function assert(b) { + if (!b) + throw new Error("Assertion Failed"); +} +function asap$1(fn) { + if (_global.setImmediate) + setImmediate(fn); + else + setTimeout(fn, 0); +} +function arrayToObject(array, extractor) { + return array.reduce((result, item, i) => { + var nameAndValue = extractor(item, i); + if (nameAndValue) + result[nameAndValue[0]] = nameAndValue[1]; + return result; + }, {}); +} +function getByKeyPath(obj, keyPath) { + if (typeof keyPath === 'string' && hasOwn(obj, keyPath)) + return obj[keyPath]; + if (!keyPath) + return obj; + if (typeof keyPath !== 'string') { + var rv = []; + for (var i = 0, l = keyPath.length; i < l; ++i) { + var val = getByKeyPath(obj, keyPath[i]); + rv.push(val); + } + return rv; + } + var period = keyPath.indexOf('.'); + if (period !== -1) { + var innerObj = obj[keyPath.substr(0, period)]; + return innerObj == null ? undefined : getByKeyPath(innerObj, keyPath.substr(period + 1)); + } + return undefined; +} +function setByKeyPath(obj, keyPath, value) { + if (!obj || keyPath === undefined) + return; + if ('isFrozen' in Object && Object.isFrozen(obj)) + return; + if (typeof keyPath !== 'string' && 'length' in keyPath) { + assert(typeof value !== 'string' && 'length' in value); + for (var i = 0, l = keyPath.length; i < l; ++i) { + setByKeyPath(obj, keyPath[i], value[i]); + } + } + else { + var period = keyPath.indexOf('.'); + if (period !== -1) { + var currentKeyPath = keyPath.substr(0, period); + var remainingKeyPath = keyPath.substr(period + 1); + if (remainingKeyPath === "") + if (value === undefined) { + if (isArray(obj) && !isNaN(parseInt(currentKeyPath))) + obj.splice(currentKeyPath, 1); + else + delete obj[currentKeyPath]; + } + else + obj[currentKeyPath] = value; + else { + var innerObj = obj[currentKeyPath]; + if (!innerObj || !hasOwn(obj, currentKeyPath)) + innerObj = (obj[currentKeyPath] = {}); + setByKeyPath(innerObj, remainingKeyPath, value); + } + } + else { + if (value === undefined) { + if (isArray(obj) && !isNaN(parseInt(keyPath))) + obj.splice(keyPath, 1); + else + delete obj[keyPath]; + } + else + obj[keyPath] = value; + } + } +} +function delByKeyPath(obj, keyPath) { + if (typeof keyPath === 'string') + setByKeyPath(obj, keyPath, undefined); + else if ('length' in keyPath) + [].map.call(keyPath, function (kp) { + setByKeyPath(obj, kp, undefined); + }); +} +function shallowClone(obj) { + var rv = {}; + for (var m in obj) { + if (hasOwn(obj, m)) + rv[m] = obj[m]; + } + return rv; +} +const concat = [].concat; +function flatten(a) { + return concat.apply([], a); +} +const intrinsicTypeNames = "BigUint64Array,BigInt64Array,Array,Boolean,String,Date,RegExp,Blob,File,FileList,FileSystemFileHandle,FileSystemDirectoryHandle,ArrayBuffer,DataView,Uint8ClampedArray,ImageBitmap,ImageData,Map,Set,CryptoKey" + .split(',').concat(flatten([8, 16, 32, 64].map(num => ["Int", "Uint", "Float"].map(t => t + num + "Array")))).filter(t => _global[t]); +const intrinsicTypes = new Set(intrinsicTypeNames.map(t => _global[t])); +function cloneSimpleObjectTree(o) { + const rv = {}; + for (const k in o) + if (hasOwn(o, k)) { + const v = o[k]; + rv[k] = !v || typeof v !== 'object' || intrinsicTypes.has(v.constructor) ? v : cloneSimpleObjectTree(v); + } + return rv; +} +function objectIsEmpty(o) { + for (const k in o) + if (hasOwn(o, k)) + return false; + return true; +} +let circularRefs = null; +function deepClone(any) { + circularRefs = new WeakMap(); + const rv = innerDeepClone(any); + circularRefs = null; + return rv; +} +function innerDeepClone(x) { + if (!x || typeof x !== 'object') + return x; + let rv = circularRefs.get(x); + if (rv) + return rv; + if (isArray(x)) { + rv = []; + circularRefs.set(x, rv); + for (var i = 0, l = x.length; i < l; ++i) { + rv.push(innerDeepClone(x[i])); + } + } + else if (intrinsicTypes.has(x.constructor)) { + rv = x; + } + else { + const proto = getProto(x); + rv = proto === Object.prototype ? {} : Object.create(proto); + circularRefs.set(x, rv); + for (var prop in x) { + if (hasOwn(x, prop)) { + rv[prop] = innerDeepClone(x[prop]); + } + } + } + return rv; +} +const { toString } = {}; +function toStringTag(o) { + return toString.call(o).slice(8, -1); +} +const iteratorSymbol = typeof Symbol !== 'undefined' ? + Symbol.iterator : + '@@iterator'; +const getIteratorOf = typeof iteratorSymbol === "symbol" ? function (x) { + var i; + return x != null && (i = x[iteratorSymbol]) && i.apply(x); +} : function () { return null; }; +function delArrayItem(a, x) { + const i = a.indexOf(x); + if (i >= 0) + a.splice(i, 1); + return i >= 0; +} +const NO_CHAR_ARRAY = {}; +function getArrayOf(arrayLike) { + var i, a, x, it; + if (arguments.length === 1) { + if (isArray(arrayLike)) + return arrayLike.slice(); + if (this === NO_CHAR_ARRAY && typeof arrayLike === 'string') + return [arrayLike]; + if ((it = getIteratorOf(arrayLike))) { + a = []; + while ((x = it.next()), !x.done) + a.push(x.value); + return a; + } + if (arrayLike == null) + return [arrayLike]; + i = arrayLike.length; + if (typeof i === 'number') { + a = new Array(i); + while (i--) + a[i] = arrayLike[i]; + return a; + } + return [arrayLike]; + } + i = arguments.length; + a = new Array(i); + while (i--) + a[i] = arguments[i]; + return a; +} +const isAsyncFunction = typeof Symbol !== 'undefined' + ? (fn) => fn[Symbol.toStringTag] === 'AsyncFunction' + : () => false; + +var dexieErrorNames = [ + 'Modify', + 'Bulk', + 'OpenFailed', + 'VersionChange', + 'Schema', + 'Upgrade', + 'InvalidTable', + 'MissingAPI', + 'NoSuchDatabase', + 'InvalidArgument', + 'SubTransaction', + 'Unsupported', + 'Internal', + 'DatabaseClosed', + 'PrematureCommit', + 'ForeignAwait' +]; +var idbDomErrorNames = [ + 'Unknown', + 'Constraint', + 'Data', + 'TransactionInactive', + 'ReadOnly', + 'Version', + 'NotFound', + 'InvalidState', + 'InvalidAccess', + 'Abort', + 'Timeout', + 'QuotaExceeded', + 'Syntax', + 'DataClone' +]; +var errorList = dexieErrorNames.concat(idbDomErrorNames); +var defaultTexts = { + VersionChanged: "Database version changed by other database connection", + DatabaseClosed: "Database has been closed", + Abort: "Transaction aborted", + TransactionInactive: "Transaction has already completed or failed", + MissingAPI: "IndexedDB API missing. Please visit https://tinyurl.com/y2uuvskb" +}; +function DexieError(name, msg) { + this.name = name; + this.message = msg; +} +derive(DexieError).from(Error).extend({ + toString: function () { return this.name + ": " + this.message; } +}); +function getMultiErrorMessage(msg, failures) { + return msg + ". Errors: " + Object.keys(failures) + .map(key => failures[key].toString()) + .filter((v, i, s) => s.indexOf(v) === i) + .join('\n'); +} +function ModifyError(msg, failures, successCount, failedKeys) { + this.failures = failures; + this.failedKeys = failedKeys; + this.successCount = successCount; + this.message = getMultiErrorMessage(msg, failures); +} +derive(ModifyError).from(DexieError); +function BulkError(msg, failures) { + this.name = "BulkError"; + this.failures = Object.keys(failures).map(pos => failures[pos]); + this.failuresByPos = failures; + this.message = getMultiErrorMessage(msg, this.failures); +} +derive(BulkError).from(DexieError); +var errnames = errorList.reduce((obj, name) => (obj[name] = name + "Error", obj), {}); +const BaseException = DexieError; +var exceptions = errorList.reduce((obj, name) => { + var fullName = name + "Error"; + function DexieError(msgOrInner, inner) { + this.name = fullName; + if (!msgOrInner) { + this.message = defaultTexts[name] || fullName; + this.inner = null; + } + else if (typeof msgOrInner === 'string') { + this.message = `${msgOrInner}${!inner ? '' : '\n ' + inner}`; + this.inner = inner || null; + } + else if (typeof msgOrInner === 'object') { + this.message = `${msgOrInner.name} ${msgOrInner.message}`; + this.inner = msgOrInner; + } + } + derive(DexieError).from(BaseException); + obj[name] = DexieError; + return obj; +}, {}); +exceptions.Syntax = SyntaxError; +exceptions.Type = TypeError; +exceptions.Range = RangeError; +var exceptionMap = idbDomErrorNames.reduce((obj, name) => { + obj[name + "Error"] = exceptions[name]; + return obj; +}, {}); +function mapError(domError, message) { + if (!domError || domError instanceof DexieError || domError instanceof TypeError || domError instanceof SyntaxError || !domError.name || !exceptionMap[domError.name]) + return domError; + var rv = new exceptionMap[domError.name](message || domError.message, domError); + if ("stack" in domError) { + setProp(rv, "stack", { get: function () { + return this.inner.stack; + } }); + } + return rv; +} +var fullNameExceptions = errorList.reduce((obj, name) => { + if (["Syntax", "Type", "Range"].indexOf(name) === -1) + obj[name + "Error"] = exceptions[name]; + return obj; +}, {}); +fullNameExceptions.ModifyError = ModifyError; +fullNameExceptions.DexieError = DexieError; +fullNameExceptions.BulkError = BulkError; + +function nop() { } +function mirror(val) { return val; } +function pureFunctionChain(f1, f2) { + if (f1 == null || f1 === mirror) + return f2; + return function (val) { + return f2(f1(val)); + }; +} +function callBoth(on1, on2) { + return function () { + on1.apply(this, arguments); + on2.apply(this, arguments); + }; +} +function hookCreatingChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + var res = f1.apply(this, arguments); + if (res !== undefined) + arguments[0] = res; + var onsuccess = this.onsuccess, + onerror = this.onerror; + this.onsuccess = null; + this.onerror = null; + var res2 = f2.apply(this, arguments); + if (onsuccess) + this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; + if (onerror) + this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; + return res2 !== undefined ? res2 : res; + }; +} +function hookDeletingChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + f1.apply(this, arguments); + var onsuccess = this.onsuccess, + onerror = this.onerror; + this.onsuccess = this.onerror = null; + f2.apply(this, arguments); + if (onsuccess) + this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; + if (onerror) + this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; + }; +} +function hookUpdatingChain(f1, f2) { + if (f1 === nop) + return f2; + return function (modifications) { + var res = f1.apply(this, arguments); + extend(modifications, res); + var onsuccess = this.onsuccess, + onerror = this.onerror; + this.onsuccess = null; + this.onerror = null; + var res2 = f2.apply(this, arguments); + if (onsuccess) + this.onsuccess = this.onsuccess ? callBoth(onsuccess, this.onsuccess) : onsuccess; + if (onerror) + this.onerror = this.onerror ? callBoth(onerror, this.onerror) : onerror; + return res === undefined ? + (res2 === undefined ? undefined : res2) : + (extend(res, res2)); + }; +} +function reverseStoppableEventChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + if (f2.apply(this, arguments) === false) + return false; + return f1.apply(this, arguments); + }; +} +function promisableChain(f1, f2) { + if (f1 === nop) + return f2; + return function () { + var res = f1.apply(this, arguments); + if (res && typeof res.then === 'function') { + var thiz = this, i = arguments.length, args = new Array(i); + while (i--) + args[i] = arguments[i]; + return res.then(function () { + return f2.apply(thiz, args); + }); + } + return f2.apply(this, arguments); + }; +} + +var debug = typeof location !== 'undefined' && + /^(http|https):\/\/(localhost|127\.0\.0\.1)/.test(location.href); +function setDebug(value, filter) { + debug = value; +} + +var INTERNAL = {}; +const ZONE_ECHO_LIMIT = 100, [resolvedNativePromise, nativePromiseProto, resolvedGlobalPromise] = typeof Promise === 'undefined' ? + [] : + (() => { + let globalP = Promise.resolve(); + if (typeof crypto === 'undefined' || !crypto.subtle) + return [globalP, getProto(globalP), globalP]; + const nativeP = crypto.subtle.digest("SHA-512", new Uint8Array([0])); + return [ + nativeP, + getProto(nativeP), + globalP + ]; + })(), nativePromiseThen = nativePromiseProto && nativePromiseProto.then; +const NativePromise = resolvedNativePromise && resolvedNativePromise.constructor; +const patchGlobalPromise = !!resolvedGlobalPromise; +function schedulePhysicalTick() { + queueMicrotask(physicalTick); +} +var asap = function (callback, args) { + microtickQueue.push([callback, args]); + if (needsNewPhysicalTick) { + schedulePhysicalTick(); + needsNewPhysicalTick = false; + } +}; +var isOutsideMicroTick = true, +needsNewPhysicalTick = true, +unhandledErrors = [], +rejectingErrors = [], +rejectionMapper = mirror; +var globalPSD = { + id: 'global', + global: true, + ref: 0, + unhandleds: [], + onunhandled: nop, + pgp: false, + env: {}, + finalize: nop +}; +var PSD = globalPSD; +var microtickQueue = []; +var numScheduledCalls = 0; +var tickFinalizers = []; +function DexiePromise(fn) { + if (typeof this !== 'object') + throw new TypeError('Promises must be constructed via new'); + this._listeners = []; + this._lib = false; + var psd = (this._PSD = PSD); + if (typeof fn !== 'function') { + if (fn !== INTERNAL) + throw new TypeError('Not a function'); + this._state = arguments[1]; + this._value = arguments[2]; + if (this._state === false) + handleRejection(this, this._value); + return; + } + this._state = null; + this._value = null; + ++psd.ref; + executePromiseTask(this, fn); +} +const thenProp = { + get: function () { + var psd = PSD, microTaskId = totalEchoes; + function then(onFulfilled, onRejected) { + var possibleAwait = !psd.global && (psd !== PSD || microTaskId !== totalEchoes); + const cleanup = possibleAwait && !decrementExpectedAwaits(); + var rv = new DexiePromise((resolve, reject) => { + propagateToListener(this, new Listener(nativeAwaitCompatibleWrap(onFulfilled, psd, possibleAwait, cleanup), nativeAwaitCompatibleWrap(onRejected, psd, possibleAwait, cleanup), resolve, reject, psd)); + }); + if (this._consoleTask) + rv._consoleTask = this._consoleTask; + return rv; + } + then.prototype = INTERNAL; + return then; + }, + set: function (value) { + setProp(this, 'then', value && value.prototype === INTERNAL ? + thenProp : + { + get: function () { + return value; + }, + set: thenProp.set + }); + } +}; +props(DexiePromise.prototype, { + then: thenProp, + _then: function (onFulfilled, onRejected) { + propagateToListener(this, new Listener(null, null, onFulfilled, onRejected, PSD)); + }, + catch: function (onRejected) { + if (arguments.length === 1) + return this.then(null, onRejected); + var type = arguments[0], handler = arguments[1]; + return typeof type === 'function' ? this.then(null, err => + err instanceof type ? handler(err) : PromiseReject(err)) + : this.then(null, err => + err && err.name === type ? handler(err) : PromiseReject(err)); + }, + finally: function (onFinally) { + return this.then(value => { + return DexiePromise.resolve(onFinally()).then(() => value); + }, err => { + return DexiePromise.resolve(onFinally()).then(() => PromiseReject(err)); + }); + }, + timeout: function (ms, msg) { + return ms < Infinity ? + new DexiePromise((resolve, reject) => { + var handle = setTimeout(() => reject(new exceptions.Timeout(msg)), ms); + this.then(resolve, reject).finally(clearTimeout.bind(null, handle)); + }) : this; + } +}); +if (typeof Symbol !== 'undefined' && Symbol.toStringTag) + setProp(DexiePromise.prototype, Symbol.toStringTag, 'Dexie.Promise'); +globalPSD.env = snapShot(); +function Listener(onFulfilled, onRejected, resolve, reject, zone) { + this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; + this.onRejected = typeof onRejected === 'function' ? onRejected : null; + this.resolve = resolve; + this.reject = reject; + this.psd = zone; +} +props(DexiePromise, { + all: function () { + var values = getArrayOf.apply(null, arguments) + .map(onPossibleParallellAsync); + return new DexiePromise(function (resolve, reject) { + if (values.length === 0) + resolve([]); + var remaining = values.length; + values.forEach((a, i) => DexiePromise.resolve(a).then(x => { + values[i] = x; + if (!--remaining) + resolve(values); + }, reject)); + }); + }, + resolve: value => { + if (value instanceof DexiePromise) + return value; + if (value && typeof value.then === 'function') + return new DexiePromise((resolve, reject) => { + value.then(resolve, reject); + }); + var rv = new DexiePromise(INTERNAL, true, value); + return rv; + }, + reject: PromiseReject, + race: function () { + var values = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); + return new DexiePromise((resolve, reject) => { + values.map(value => DexiePromise.resolve(value).then(resolve, reject)); + }); + }, + PSD: { + get: () => PSD, + set: value => PSD = value + }, + totalEchoes: { get: () => totalEchoes }, + newPSD: newScope, + usePSD: usePSD, + scheduler: { + get: () => asap, + set: value => { asap = value; } + }, + rejectionMapper: { + get: () => rejectionMapper, + set: value => { rejectionMapper = value; } + }, + follow: (fn, zoneProps) => { + return new DexiePromise((resolve, reject) => { + return newScope((resolve, reject) => { + var psd = PSD; + psd.unhandleds = []; + psd.onunhandled = reject; + psd.finalize = callBoth(function () { + run_at_end_of_this_or_next_physical_tick(() => { + this.unhandleds.length === 0 ? resolve() : reject(this.unhandleds[0]); + }); + }, psd.finalize); + fn(); + }, zoneProps, resolve, reject); + }); + } +}); +if (NativePromise) { + if (NativePromise.allSettled) + setProp(DexiePromise, "allSettled", function () { + const possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); + return new DexiePromise(resolve => { + if (possiblePromises.length === 0) + resolve([]); + let remaining = possiblePromises.length; + const results = new Array(remaining); + possiblePromises.forEach((p, i) => DexiePromise.resolve(p).then(value => results[i] = { status: "fulfilled", value }, reason => results[i] = { status: "rejected", reason }) + .then(() => --remaining || resolve(results))); + }); + }); + if (NativePromise.any && typeof AggregateError !== 'undefined') + setProp(DexiePromise, "any", function () { + const possiblePromises = getArrayOf.apply(null, arguments).map(onPossibleParallellAsync); + return new DexiePromise((resolve, reject) => { + if (possiblePromises.length === 0) + reject(new AggregateError([])); + let remaining = possiblePromises.length; + const failures = new Array(remaining); + possiblePromises.forEach((p, i) => DexiePromise.resolve(p).then(value => resolve(value), failure => { + failures[i] = failure; + if (!--remaining) + reject(new AggregateError(failures)); + })); + }); + }); + if (NativePromise.withResolvers) + DexiePromise.withResolvers = NativePromise.withResolvers; +} +function executePromiseTask(promise, fn) { + try { + fn(value => { + if (promise._state !== null) + return; + if (value === promise) + throw new TypeError('A promise cannot be resolved with itself.'); + var shouldExecuteTick = promise._lib && beginMicroTickScope(); + if (value && typeof value.then === 'function') { + executePromiseTask(promise, (resolve, reject) => { + value instanceof DexiePromise ? + value._then(resolve, reject) : + value.then(resolve, reject); + }); + } + else { + promise._state = true; + promise._value = value; + propagateAllListeners(promise); + } + if (shouldExecuteTick) + endMicroTickScope(); + }, handleRejection.bind(null, promise)); + } + catch (ex) { + handleRejection(promise, ex); + } +} +function handleRejection(promise, reason) { + rejectingErrors.push(reason); + if (promise._state !== null) + return; + var shouldExecuteTick = promise._lib && beginMicroTickScope(); + reason = rejectionMapper(reason); + promise._state = false; + promise._value = reason; + addPossiblyUnhandledError(promise); + propagateAllListeners(promise); + if (shouldExecuteTick) + endMicroTickScope(); +} +function propagateAllListeners(promise) { + var listeners = promise._listeners; + promise._listeners = []; + for (var i = 0, len = listeners.length; i < len; ++i) { + propagateToListener(promise, listeners[i]); + } + var psd = promise._PSD; + --psd.ref || psd.finalize(); + if (numScheduledCalls === 0) { + ++numScheduledCalls; + asap(() => { + if (--numScheduledCalls === 0) + finalizePhysicalTick(); + }, []); + } +} +function propagateToListener(promise, listener) { + if (promise._state === null) { + promise._listeners.push(listener); + return; + } + var cb = promise._state ? listener.onFulfilled : listener.onRejected; + if (cb === null) { + return (promise._state ? listener.resolve : listener.reject)(promise._value); + } + ++listener.psd.ref; + ++numScheduledCalls; + asap(callListener, [cb, promise, listener]); +} +function callListener(cb, promise, listener) { + try { + var ret, value = promise._value; + if (!promise._state && rejectingErrors.length) + rejectingErrors = []; + ret = debug && promise._consoleTask ? promise._consoleTask.run(() => cb(value)) : cb(value); + if (!promise._state && rejectingErrors.indexOf(value) === -1) { + markErrorAsHandled(promise); + } + listener.resolve(ret); + } + catch (e) { + listener.reject(e); + } + finally { + if (--numScheduledCalls === 0) + finalizePhysicalTick(); + --listener.psd.ref || listener.psd.finalize(); + } +} +function physicalTick() { + usePSD(globalPSD, () => { + beginMicroTickScope() && endMicroTickScope(); + }); +} +function beginMicroTickScope() { + var wasRootExec = isOutsideMicroTick; + isOutsideMicroTick = false; + needsNewPhysicalTick = false; + return wasRootExec; +} +function endMicroTickScope() { + var callbacks, i, l; + do { + while (microtickQueue.length > 0) { + callbacks = microtickQueue; + microtickQueue = []; + l = callbacks.length; + for (i = 0; i < l; ++i) { + var item = callbacks[i]; + item[0].apply(null, item[1]); + } + } + } while (microtickQueue.length > 0); + isOutsideMicroTick = true; + needsNewPhysicalTick = true; +} +function finalizePhysicalTick() { + var unhandledErrs = unhandledErrors; + unhandledErrors = []; + unhandledErrs.forEach(p => { + p._PSD.onunhandled.call(null, p._value, p); + }); + var finalizers = tickFinalizers.slice(0); + var i = finalizers.length; + while (i) + finalizers[--i](); +} +function run_at_end_of_this_or_next_physical_tick(fn) { + function finalizer() { + fn(); + tickFinalizers.splice(tickFinalizers.indexOf(finalizer), 1); + } + tickFinalizers.push(finalizer); + ++numScheduledCalls; + asap(() => { + if (--numScheduledCalls === 0) + finalizePhysicalTick(); + }, []); +} +function addPossiblyUnhandledError(promise) { + if (!unhandledErrors.some(p => p._value === promise._value)) + unhandledErrors.push(promise); +} +function markErrorAsHandled(promise) { + var i = unhandledErrors.length; + while (i) + if (unhandledErrors[--i]._value === promise._value) { + unhandledErrors.splice(i, 1); + return; + } +} +function PromiseReject(reason) { + return new DexiePromise(INTERNAL, false, reason); +} +function wrap(fn, errorCatcher) { + var psd = PSD; + return function () { + var wasRootExec = beginMicroTickScope(), outerScope = PSD; + try { + switchToZone(psd, true); + return fn.apply(this, arguments); + } + catch (e) { + errorCatcher && errorCatcher(e); + } + finally { + switchToZone(outerScope, false); + if (wasRootExec) + endMicroTickScope(); + } + }; +} +const task = { awaits: 0, echoes: 0, id: 0 }; +var taskCounter = 0; +var zoneStack = []; +var zoneEchoes = 0; +var totalEchoes = 0; +var zone_id_counter = 0; +function newScope(fn, props, a1, a2) { + var parent = PSD, psd = Object.create(parent); + psd.parent = parent; + psd.ref = 0; + psd.global = false; + psd.id = ++zone_id_counter; + globalPSD.env; + psd.env = patchGlobalPromise ? { + Promise: DexiePromise, + PromiseProp: { value: DexiePromise, configurable: true, writable: true }, + all: DexiePromise.all, + race: DexiePromise.race, + allSettled: DexiePromise.allSettled, + any: DexiePromise.any, + resolve: DexiePromise.resolve, + reject: DexiePromise.reject, + } : {}; + if (props) + extend(psd, props); + ++parent.ref; + psd.finalize = function () { + --this.parent.ref || this.parent.finalize(); + }; + var rv = usePSD(psd, fn, a1, a2); + if (psd.ref === 0) + psd.finalize(); + return rv; +} +function incrementExpectedAwaits() { + if (!task.id) + task.id = ++taskCounter; + ++task.awaits; + task.echoes += ZONE_ECHO_LIMIT; + return task.id; +} +function decrementExpectedAwaits() { + if (!task.awaits) + return false; + if (--task.awaits === 0) + task.id = 0; + task.echoes = task.awaits * ZONE_ECHO_LIMIT; + return true; +} +if (('' + nativePromiseThen).indexOf('[native code]') === -1) { + incrementExpectedAwaits = decrementExpectedAwaits = nop; +} +function onPossibleParallellAsync(possiblePromise) { + if (task.echoes && possiblePromise && possiblePromise.constructor === NativePromise) { + incrementExpectedAwaits(); + return possiblePromise.then(x => { + decrementExpectedAwaits(); + return x; + }, e => { + decrementExpectedAwaits(); + return rejection(e); + }); + } + return possiblePromise; +} +function zoneEnterEcho(targetZone) { + ++totalEchoes; + if (!task.echoes || --task.echoes === 0) { + task.echoes = task.awaits = task.id = 0; + } + zoneStack.push(PSD); + switchToZone(targetZone, true); +} +function zoneLeaveEcho() { + var zone = zoneStack[zoneStack.length - 1]; + zoneStack.pop(); + switchToZone(zone, false); +} +function switchToZone(targetZone, bEnteringZone) { + var currentZone = PSD; + if (bEnteringZone ? task.echoes && (!zoneEchoes++ || targetZone !== PSD) : zoneEchoes && (!--zoneEchoes || targetZone !== PSD)) { + queueMicrotask(bEnteringZone ? zoneEnterEcho.bind(null, targetZone) : zoneLeaveEcho); + } + if (targetZone === PSD) + return; + PSD = targetZone; + if (currentZone === globalPSD) + globalPSD.env = snapShot(); + if (patchGlobalPromise) { + var GlobalPromise = globalPSD.env.Promise; + var targetEnv = targetZone.env; + if (currentZone.global || targetZone.global) { + Object.defineProperty(_global, 'Promise', targetEnv.PromiseProp); + GlobalPromise.all = targetEnv.all; + GlobalPromise.race = targetEnv.race; + GlobalPromise.resolve = targetEnv.resolve; + GlobalPromise.reject = targetEnv.reject; + if (targetEnv.allSettled) + GlobalPromise.allSettled = targetEnv.allSettled; + if (targetEnv.any) + GlobalPromise.any = targetEnv.any; + } + } +} +function snapShot() { + var GlobalPromise = _global.Promise; + return patchGlobalPromise ? { + Promise: GlobalPromise, + PromiseProp: Object.getOwnPropertyDescriptor(_global, "Promise"), + all: GlobalPromise.all, + race: GlobalPromise.race, + allSettled: GlobalPromise.allSettled, + any: GlobalPromise.any, + resolve: GlobalPromise.resolve, + reject: GlobalPromise.reject, + } : {}; +} +function usePSD(psd, fn, a1, a2, a3) { + var outerScope = PSD; + try { + switchToZone(psd, true); + return fn(a1, a2, a3); + } + finally { + switchToZone(outerScope, false); + } +} +function nativeAwaitCompatibleWrap(fn, zone, possibleAwait, cleanup) { + return typeof fn !== 'function' ? fn : function () { + var outerZone = PSD; + if (possibleAwait) + incrementExpectedAwaits(); + switchToZone(zone, true); + try { + return fn.apply(this, arguments); + } + finally { + switchToZone(outerZone, false); + if (cleanup) + queueMicrotask(decrementExpectedAwaits); + } + }; +} +function execInGlobalContext(cb) { + if (Promise === NativePromise && task.echoes === 0) { + if (zoneEchoes === 0) { + cb(); + } + else { + enqueueNativeMicroTask(cb); + } + } + else { + setTimeout(cb, 0); + } +} +var rejection = DexiePromise.reject; + +function tempTransaction(db, mode, storeNames, fn) { + if (!db.idbdb || (!db._state.openComplete && (!PSD.letThrough && !db._vip))) { + if (db._state.openComplete) { + return rejection(new exceptions.DatabaseClosed(db._state.dbOpenError)); + } + if (!db._state.isBeingOpened) { + if (!db._state.autoOpen) + return rejection(new exceptions.DatabaseClosed()); + db.open().catch(nop); + } + return db._state.dbReadyPromise.then(() => tempTransaction(db, mode, storeNames, fn)); + } + else { + var trans = db._createTransaction(mode, storeNames, db._dbSchema); + try { + trans.create(); + db._state.PR1398_maxLoop = 3; + } + catch (ex) { + if (ex.name === errnames.InvalidState && db.isOpen() && --db._state.PR1398_maxLoop > 0) { + console.warn('Dexie: Need to reopen db'); + db.close({ disableAutoOpen: false }); + return db.open().then(() => tempTransaction(db, mode, storeNames, fn)); + } + return rejection(ex); + } + return trans._promise(mode, (resolve, reject) => { + return newScope(() => { + PSD.trans = trans; + return fn(resolve, reject, trans); + }); + }).then(result => { + if (mode === 'readwrite') + try { + trans.idbtrans.commit(); + } + catch { } + return mode === 'readonly' ? result : trans._completion.then(() => result); + }); + } +} + +const DEXIE_VERSION = '4.2.1'; +const maxString = String.fromCharCode(65535); +const minKey = -Infinity; +const INVALID_KEY_ARGUMENT = "Invalid key provided. Keys must be of type string, number, Date or Array."; +const STRING_EXPECTED = "String expected."; +const connections = []; +const DBNAMES_DB = '__dbnames'; +const READONLY = 'readonly'; +const READWRITE = 'readwrite'; + +function combine(filter1, filter2) { + return filter1 ? + filter2 ? + function () { return filter1.apply(this, arguments) && filter2.apply(this, arguments); } : + filter1 : + filter2; +} + +const AnyRange = { + type: 3 , + lower: -Infinity, + lowerOpen: false, + upper: [[]], + upperOpen: false +}; + +function workaroundForUndefinedPrimKey(keyPath) { + return typeof keyPath === "string" && !/\./.test(keyPath) + ? (obj) => { + if (obj[keyPath] === undefined && (keyPath in obj)) { + obj = deepClone(obj); + delete obj[keyPath]; + } + return obj; + } + : (obj) => obj; +} + +function Entity() { + throw exceptions.Type(`Entity instances must never be new:ed. Instances are generated by the framework bypassing the constructor.`); +} + +function cmp(a, b) { + try { + const ta = type(a); + const tb = type(b); + if (ta !== tb) { + if (ta === 'Array') + return 1; + if (tb === 'Array') + return -1; + if (ta === 'binary') + return 1; + if (tb === 'binary') + return -1; + if (ta === 'string') + return 1; + if (tb === 'string') + return -1; + if (ta === 'Date') + return 1; + if (tb !== 'Date') + return NaN; + return -1; + } + switch (ta) { + case 'number': + case 'Date': + case 'string': + return a > b ? 1 : a < b ? -1 : 0; + case 'binary': { + return compareUint8Arrays(getUint8Array(a), getUint8Array(b)); + } + case 'Array': + return compareArrays(a, b); + } + } + catch { } + return NaN; +} +function compareArrays(a, b) { + const al = a.length; + const bl = b.length; + const l = al < bl ? al : bl; + for (let i = 0; i < l; ++i) { + const res = cmp(a[i], b[i]); + if (res !== 0) + return res; + } + return al === bl ? 0 : al < bl ? -1 : 1; +} +function compareUint8Arrays(a, b) { + const al = a.length; + const bl = b.length; + const l = al < bl ? al : bl; + for (let i = 0; i < l; ++i) { + if (a[i] !== b[i]) + return a[i] < b[i] ? -1 : 1; + } + return al === bl ? 0 : al < bl ? -1 : 1; +} +function type(x) { + const t = typeof x; + if (t !== 'object') + return t; + if (ArrayBuffer.isView(x)) + return 'binary'; + const tsTag = toStringTag(x); + return tsTag === 'ArrayBuffer' ? 'binary' : tsTag; +} +function getUint8Array(a) { + if (a instanceof Uint8Array) + return a; + if (ArrayBuffer.isView(a)) + return new Uint8Array(a.buffer, a.byteOffset, a.byteLength); + return new Uint8Array(a); +} + +function builtInDeletionTrigger(table, keys, res) { + const { yProps } = table.schema; + if (!yProps) + return res; + if (keys && res.numFailures > 0) + keys = keys.filter((_, i) => !res.failures[i]); + return Promise.all(yProps.map(({ updatesTable }) => keys + ? table.db.table(updatesTable).where('k').anyOf(keys).delete() + : table.db.table(updatesTable).clear())).then(() => res); +} + +class PropModification { + execute(value) { + const spec = this["@@propmod"]; + if (spec.add !== undefined) { + const term = spec.add; + if (isArray(term)) { + return [...(isArray(value) ? value : []), ...term].sort(); + } + if (typeof term === 'number') + return (Number(value) || 0) + term; + if (typeof term === 'bigint') { + try { + return BigInt(value) + term; + } + catch { + return BigInt(0) + term; + } + } + throw new TypeError(`Invalid term ${term}`); + } + if (spec.remove !== undefined) { + const subtrahend = spec.remove; + if (isArray(subtrahend)) { + return isArray(value) ? value.filter(item => !subtrahend.includes(item)).sort() : []; + } + if (typeof subtrahend === 'number') + return Number(value) - subtrahend; + if (typeof subtrahend === 'bigint') { + try { + return BigInt(value) - subtrahend; + } + catch { + return BigInt(0) - subtrahend; + } + } + throw new TypeError(`Invalid subtrahend ${subtrahend}`); + } + const prefixToReplace = spec.replacePrefix?.[0]; + if (prefixToReplace && typeof value === 'string' && value.startsWith(prefixToReplace)) { + return spec.replacePrefix[1] + value.substring(prefixToReplace.length); + } + return value; + } + constructor(spec) { + this["@@propmod"] = spec; + } +} + +function applyUpdateSpec(obj, changes) { + const keyPaths = keys(changes); + const numKeys = keyPaths.length; + let anythingModified = false; + for (let i = 0; i < numKeys; ++i) { + const keyPath = keyPaths[i]; + const value = changes[keyPath]; + const origValue = getByKeyPath(obj, keyPath); + if (value instanceof PropModification) { + setByKeyPath(obj, keyPath, value.execute(origValue)); + anythingModified = true; + } + else if (origValue !== value) { + setByKeyPath(obj, keyPath, value); + anythingModified = true; + } + } + return anythingModified; +} + +class Table { + _trans(mode, fn, writeLocked) { + const trans = this._tx || PSD.trans; + const tableName = this.name; + const task = debug && typeof console !== 'undefined' && console.createTask && console.createTask(`Dexie: ${mode === 'readonly' ? 'read' : 'write'} ${this.name}`); + function checkTableInTransaction(resolve, reject, trans) { + if (!trans.schema[tableName]) + throw new exceptions.NotFound("Table " + tableName + " not part of transaction"); + return fn(trans.idbtrans, trans); + } + const wasRootExec = beginMicroTickScope(); + try { + let p = trans && trans.db._novip === this.db._novip ? + trans === PSD.trans ? + trans._promise(mode, checkTableInTransaction, writeLocked) : + newScope(() => trans._promise(mode, checkTableInTransaction, writeLocked), { trans: trans, transless: PSD.transless || PSD }) : + tempTransaction(this.db, mode, [this.name], checkTableInTransaction); + if (task) { + p._consoleTask = task; + p = p.catch(err => { + console.trace(err); + return rejection(err); + }); + } + return p; + } + finally { + if (wasRootExec) + endMicroTickScope(); + } + } + get(keyOrCrit, cb) { + if (keyOrCrit && keyOrCrit.constructor === Object) + return this.where(keyOrCrit).first(cb); + if (keyOrCrit == null) + return rejection(new exceptions.Type(`Invalid argument to Table.get()`)); + return this._trans('readonly', (trans) => { + return this.core.get({ trans, key: keyOrCrit }) + .then(res => this.hook.reading.fire(res)); + }).then(cb); + } + where(indexOrCrit) { + if (typeof indexOrCrit === 'string') + return new this.db.WhereClause(this, indexOrCrit); + if (isArray(indexOrCrit)) + return new this.db.WhereClause(this, `[${indexOrCrit.join('+')}]`); + const keyPaths = keys(indexOrCrit); + if (keyPaths.length === 1) + return this + .where(keyPaths[0]) + .equals(indexOrCrit[keyPaths[0]]); + const compoundIndex = this.schema.indexes.concat(this.schema.primKey).filter(ix => { + if (ix.compound && + keyPaths.every(keyPath => ix.keyPath.indexOf(keyPath) >= 0)) { + for (let i = 0; i < keyPaths.length; ++i) { + if (keyPaths.indexOf(ix.keyPath[i]) === -1) + return false; + } + return true; + } + return false; + }).sort((a, b) => a.keyPath.length - b.keyPath.length)[0]; + if (compoundIndex && this.db._maxKey !== maxString) { + const keyPathsInValidOrder = compoundIndex.keyPath.slice(0, keyPaths.length); + return this + .where(keyPathsInValidOrder) + .equals(keyPathsInValidOrder.map(kp => indexOrCrit[kp])); + } + if (!compoundIndex && debug) + console.warn(`The query ${JSON.stringify(indexOrCrit)} on ${this.name} would benefit from a ` + + `compound index [${keyPaths.join('+')}]`); + const { idxByName } = this.schema; + function equals(a, b) { + return cmp(a, b) === 0; + } + const [idx, filterFunction] = keyPaths.reduce(([prevIndex, prevFilterFn], keyPath) => { + const index = idxByName[keyPath]; + const value = indexOrCrit[keyPath]; + return [ + prevIndex || index, + prevIndex || !index ? + combine(prevFilterFn, index && index.multi ? + x => { + const prop = getByKeyPath(x, keyPath); + return isArray(prop) && prop.some(item => equals(value, item)); + } : x => equals(value, getByKeyPath(x, keyPath))) + : prevFilterFn + ]; + }, [null, null]); + return idx ? + this.where(idx.name).equals(indexOrCrit[idx.keyPath]) + .filter(filterFunction) : + compoundIndex ? + this.filter(filterFunction) : + this.where(keyPaths).equals(''); + } + filter(filterFunction) { + return this.toCollection().and(filterFunction); + } + count(thenShortcut) { + return this.toCollection().count(thenShortcut); + } + offset(offset) { + return this.toCollection().offset(offset); + } + limit(numRows) { + return this.toCollection().limit(numRows); + } + each(callback) { + return this.toCollection().each(callback); + } + toArray(thenShortcut) { + return this.toCollection().toArray(thenShortcut); + } + toCollection() { + return new this.db.Collection(new this.db.WhereClause(this)); + } + orderBy(index) { + return new this.db.Collection(new this.db.WhereClause(this, isArray(index) ? + `[${index.join('+')}]` : + index)); + } + reverse() { + return this.toCollection().reverse(); + } + mapToClass(constructor) { + const { db, name: tableName } = this; + this.schema.mappedClass = constructor; + if (constructor.prototype instanceof Entity) { + constructor = class extends constructor { + get db() { return db; } + table() { return tableName; } + }; + } + const inheritedProps = new Set(); + for (let proto = constructor.prototype; proto; proto = getProto(proto)) { + Object.getOwnPropertyNames(proto).forEach(propName => inheritedProps.add(propName)); + } + const readHook = (obj) => { + if (!obj) + return obj; + const res = Object.create(constructor.prototype); + for (let m in obj) + if (!inheritedProps.has(m)) + try { + res[m] = obj[m]; + } + catch (_) { } + return res; + }; + if (this.schema.readHook) { + this.hook.reading.unsubscribe(this.schema.readHook); + } + this.schema.readHook = readHook; + this.hook("reading", readHook); + return constructor; + } + defineClass() { + function Class(content) { + extend(this, content); + } + return this.mapToClass(Class); + } + add(obj, key) { + const { auto, keyPath } = this.schema.primKey; + let objToAdd = obj; + if (keyPath && auto) { + objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj); + } + return this._trans('readwrite', trans => { + return this.core.mutate({ trans, type: 'add', keys: key != null ? [key] : null, values: [objToAdd] }); + }).then(res => res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult) + .then(lastResult => { + if (keyPath) { + try { + setByKeyPath(obj, keyPath, lastResult); + } + catch (_) { } + } + return lastResult; + }); + } + upsert(key, modifications) { + const { keyPath } = this.schema.primKey; + return this._trans('readwrite', trans => { + return this.core.get({ trans, key }).then(existing => { + const obj = existing ?? {}; + applyUpdateSpec(obj, modifications); + if (keyPath) + setByKeyPath(obj, keyPath, key); + return this.core.mutate({ + trans, + type: 'put', + values: [obj], + keys: [key], + upsert: true, + updates: { keys: [key], changeSpecs: [modifications] } + }).then(res => res.numFailures ? DexiePromise.reject(res.failures[0]) : !!existing); + }); + }); + } + update(keyOrObject, modifications) { + if (typeof keyOrObject === 'object' && !isArray(keyOrObject)) { + const key = getByKeyPath(keyOrObject, this.schema.primKey.keyPath); + if (key === undefined) + return rejection(new exceptions.InvalidArgument("Given object does not contain its primary key")); + return this.where(":id").equals(key).modify(modifications); + } + else { + return this.where(":id").equals(keyOrObject).modify(modifications); + } + } + put(obj, key) { + const { auto, keyPath } = this.schema.primKey; + let objToAdd = obj; + if (keyPath && auto) { + objToAdd = workaroundForUndefinedPrimKey(keyPath)(obj); + } + return this._trans('readwrite', trans => this.core.mutate({ trans, type: 'put', values: [objToAdd], keys: key != null ? [key] : null })) + .then(res => res.numFailures ? DexiePromise.reject(res.failures[0]) : res.lastResult) + .then(lastResult => { + if (keyPath) { + try { + setByKeyPath(obj, keyPath, lastResult); + } + catch (_) { } + } + return lastResult; + }); + } + delete(key) { + return this._trans('readwrite', trans => this.core.mutate({ trans, type: 'delete', keys: [key] }) + .then(res => builtInDeletionTrigger(this, [key], res)) + .then(res => res.numFailures ? DexiePromise.reject(res.failures[0]) : undefined)); + } + clear() { + return this._trans('readwrite', trans => this.core.mutate({ trans, type: 'deleteRange', range: AnyRange }) + .then(res => builtInDeletionTrigger(this, null, res))) + .then(res => res.numFailures ? DexiePromise.reject(res.failures[0]) : undefined); + } + bulkGet(keys) { + return this._trans('readonly', trans => { + return this.core.getMany({ + keys, + trans + }).then(result => result.map(res => this.hook.reading.fire(res))); + }); + } + bulkAdd(objects, keysOrOptions, options) { + const keys = Array.isArray(keysOrOptions) ? keysOrOptions : undefined; + options = options || (keys ? undefined : keysOrOptions); + const wantResults = options ? options.allKeys : undefined; + return this._trans('readwrite', trans => { + const { auto, keyPath } = this.schema.primKey; + if (keyPath && keys) + throw new exceptions.InvalidArgument("bulkAdd(): keys argument invalid on tables with inbound keys"); + if (keys && keys.length !== objects.length) + throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length"); + const numObjects = objects.length; + let objectsToAdd = keyPath && auto ? + objects.map(workaroundForUndefinedPrimKey(keyPath)) : + objects; + return this.core.mutate({ trans, type: 'add', keys: keys, values: objectsToAdd, wantResults }) + .then(({ numFailures, results, lastResult, failures }) => { + const result = wantResults ? results : lastResult; + if (numFailures === 0) + return result; + throw new BulkError(`${this.name}.bulkAdd(): ${numFailures} of ${numObjects} operations failed`, failures); + }); + }); + } + bulkPut(objects, keysOrOptions, options) { + const keys = Array.isArray(keysOrOptions) ? keysOrOptions : undefined; + options = options || (keys ? undefined : keysOrOptions); + const wantResults = options ? options.allKeys : undefined; + return this._trans('readwrite', trans => { + const { auto, keyPath } = this.schema.primKey; + if (keyPath && keys) + throw new exceptions.InvalidArgument("bulkPut(): keys argument invalid on tables with inbound keys"); + if (keys && keys.length !== objects.length) + throw new exceptions.InvalidArgument("Arguments objects and keys must have the same length"); + const numObjects = objects.length; + let objectsToPut = keyPath && auto ? + objects.map(workaroundForUndefinedPrimKey(keyPath)) : + objects; + return this.core.mutate({ trans, type: 'put', keys: keys, values: objectsToPut, wantResults }) + .then(({ numFailures, results, lastResult, failures }) => { + const result = wantResults ? results : lastResult; + if (numFailures === 0) + return result; + throw new BulkError(`${this.name}.bulkPut(): ${numFailures} of ${numObjects} operations failed`, failures); + }); + }); + } + bulkUpdate(keysAndChanges) { + const coreTable = this.core; + const keys = keysAndChanges.map((entry) => entry.key); + const changeSpecs = keysAndChanges.map((entry) => entry.changes); + const offsetMap = []; + return this._trans('readwrite', (trans) => { + return coreTable.getMany({ trans, keys, cache: 'clone' }).then((objs) => { + const resultKeys = []; + const resultObjs = []; + keysAndChanges.forEach(({ key, changes }, idx) => { + const obj = objs[idx]; + if (obj) { + for (const keyPath of Object.keys(changes)) { + const value = changes[keyPath]; + if (keyPath === this.schema.primKey.keyPath) { + if (cmp(value, key) !== 0) { + throw new exceptions.Constraint(`Cannot update primary key in bulkUpdate()`); + } + } + else { + setByKeyPath(obj, keyPath, value); + } + } + offsetMap.push(idx); + resultKeys.push(key); + resultObjs.push(obj); + } + }); + const numEntries = resultKeys.length; + return coreTable + .mutate({ + trans, + type: 'put', + keys: resultKeys, + values: resultObjs, + updates: { + keys, + changeSpecs + } + }) + .then(({ numFailures, failures }) => { + if (numFailures === 0) + return numEntries; + for (const offset of Object.keys(failures)) { + const mappedOffset = offsetMap[Number(offset)]; + if (mappedOffset != null) { + const failure = failures[offset]; + delete failures[offset]; + failures[mappedOffset] = failure; + } + } + throw new BulkError(`${this.name}.bulkUpdate(): ${numFailures} of ${numEntries} operations failed`, failures); + }); + }); + }); + } + bulkDelete(keys) { + const numKeys = keys.length; + return this._trans('readwrite', trans => { + return this.core.mutate({ trans, type: 'delete', keys: keys }) + .then(res => builtInDeletionTrigger(this, keys, res)); + }).then(({ numFailures, lastResult, failures }) => { + if (numFailures === 0) + return lastResult; + throw new BulkError(`${this.name}.bulkDelete(): ${numFailures} of ${numKeys} operations failed`, failures); + }); + } +} + +function Events(ctx) { + var evs = {}; + var rv = function (eventName, subscriber) { + if (subscriber) { + var i = arguments.length, args = new Array(i - 1); + while (--i) + args[i - 1] = arguments[i]; + evs[eventName].subscribe.apply(null, args); + return ctx; + } + else if (typeof (eventName) === 'string') { + return evs[eventName]; + } + }; + rv.addEventType = add; + for (var i = 1, l = arguments.length; i < l; ++i) { + add(arguments[i]); + } + return rv; + function add(eventName, chainFunction, defaultFunction) { + if (typeof eventName === 'object') + return addConfiguredEvents(eventName); + if (!chainFunction) + chainFunction = reverseStoppableEventChain; + if (!defaultFunction) + defaultFunction = nop; + var context = { + subscribers: [], + fire: defaultFunction, + subscribe: function (cb) { + if (context.subscribers.indexOf(cb) === -1) { + context.subscribers.push(cb); + context.fire = chainFunction(context.fire, cb); + } + }, + unsubscribe: function (cb) { + context.subscribers = context.subscribers.filter(function (fn) { return fn !== cb; }); + context.fire = context.subscribers.reduce(chainFunction, defaultFunction); + } + }; + evs[eventName] = rv[eventName] = context; + return context; + } + function addConfiguredEvents(cfg) { + keys(cfg).forEach(function (eventName) { + var args = cfg[eventName]; + if (isArray(args)) { + add(eventName, cfg[eventName][0], cfg[eventName][1]); + } + else if (args === 'asap') { + var context = add(eventName, mirror, function fire() { + var i = arguments.length, args = new Array(i); + while (i--) + args[i] = arguments[i]; + context.subscribers.forEach(function (fn) { + asap$1(function fireEvent() { + fn.apply(null, args); + }); + }); + }); + } + else + throw new exceptions.InvalidArgument("Invalid event config"); + }); + } +} + +function makeClassConstructor(prototype, constructor) { + derive(constructor).from({ prototype }); + return constructor; +} + +function createTableConstructor(db) { + return makeClassConstructor(Table.prototype, function Table(name, tableSchema, trans) { + this.db = db; + this._tx = trans; + this.name = name; + this.schema = tableSchema; + this.hook = db._allTables[name] ? db._allTables[name].hook : Events(null, { + "creating": [hookCreatingChain, nop], + "reading": [pureFunctionChain, mirror], + "updating": [hookUpdatingChain, nop], + "deleting": [hookDeletingChain, nop] + }); + }); +} + +function isPlainKeyRange(ctx, ignoreLimitFilter) { + return !(ctx.filter || ctx.algorithm || ctx.or) && + (ignoreLimitFilter ? ctx.justLimit : !ctx.replayFilter); +} +function addFilter(ctx, fn) { + ctx.filter = combine(ctx.filter, fn); +} +function addReplayFilter(ctx, factory, isLimitFilter) { + var curr = ctx.replayFilter; + ctx.replayFilter = curr ? () => combine(curr(), factory()) : factory; + ctx.justLimit = isLimitFilter && !curr; +} +function addMatchFilter(ctx, fn) { + ctx.isMatch = combine(ctx.isMatch, fn); +} +function getIndexOrStore(ctx, coreSchema) { + if (ctx.isPrimKey) + return coreSchema.primaryKey; + const index = coreSchema.getIndexByKeyPath(ctx.index); + if (!index) + throw new exceptions.Schema("KeyPath " + ctx.index + " on object store " + coreSchema.name + " is not indexed"); + return index; +} +function openCursor(ctx, coreTable, trans) { + const index = getIndexOrStore(ctx, coreTable.schema); + return coreTable.openCursor({ + trans, + values: !ctx.keysOnly, + reverse: ctx.dir === 'prev', + unique: !!ctx.unique, + query: { + index, + range: ctx.range + } + }); +} +function iter(ctx, fn, coreTrans, coreTable) { + const filter = ctx.replayFilter ? combine(ctx.filter, ctx.replayFilter()) : ctx.filter; + if (!ctx.or) { + return iterate(openCursor(ctx, coreTable, coreTrans), combine(ctx.algorithm, filter), fn, !ctx.keysOnly && ctx.valueMapper); + } + else { + const set = {}; + const union = (item, cursor, advance) => { + if (!filter || filter(cursor, advance, result => cursor.stop(result), err => cursor.fail(err))) { + var primaryKey = cursor.primaryKey; + var key = '' + primaryKey; + if (key === '[object ArrayBuffer]') + key = '' + new Uint8Array(primaryKey); + if (!hasOwn(set, key)) { + set[key] = true; + fn(item, cursor, advance); + } + } + }; + return Promise.all([ + ctx.or._iterate(union, coreTrans), + iterate(openCursor(ctx, coreTable, coreTrans), ctx.algorithm, union, !ctx.keysOnly && ctx.valueMapper) + ]); + } +} +function iterate(cursorPromise, filter, fn, valueMapper) { + var mappedFn = valueMapper ? (x, c, a) => fn(valueMapper(x), c, a) : fn; + var wrappedFn = wrap(mappedFn); + return cursorPromise.then(cursor => { + if (cursor) { + return cursor.start(() => { + var c = () => cursor.continue(); + if (!filter || filter(cursor, advancer => c = advancer, val => { cursor.stop(val); c = nop; }, e => { cursor.fail(e); c = nop; })) + wrappedFn(cursor.value, cursor, advancer => c = advancer); + c(); + }); + } + }); +} + +class Collection { + _read(fn, cb) { + var ctx = this._ctx; + return ctx.error ? + ctx.table._trans(null, rejection.bind(null, ctx.error)) : + ctx.table._trans('readonly', fn).then(cb); + } + _write(fn) { + var ctx = this._ctx; + return ctx.error ? + ctx.table._trans(null, rejection.bind(null, ctx.error)) : + ctx.table._trans('readwrite', fn, "locked"); + } + _addAlgorithm(fn) { + var ctx = this._ctx; + ctx.algorithm = combine(ctx.algorithm, fn); + } + _iterate(fn, coreTrans) { + return iter(this._ctx, fn, coreTrans, this._ctx.table.core); + } + clone(props) { + var rv = Object.create(this.constructor.prototype), ctx = Object.create(this._ctx); + if (props) + extend(ctx, props); + rv._ctx = ctx; + return rv; + } + raw() { + this._ctx.valueMapper = null; + return this; + } + each(fn) { + var ctx = this._ctx; + return this._read(trans => iter(ctx, fn, trans, ctx.table.core)); + } + count(cb) { + return this._read(trans => { + const ctx = this._ctx; + const coreTable = ctx.table.core; + if (isPlainKeyRange(ctx, true)) { + return coreTable.count({ + trans, + query: { + index: getIndexOrStore(ctx, coreTable.schema), + range: ctx.range + } + }).then(count => Math.min(count, ctx.limit)); + } + else { + var count = 0; + return iter(ctx, () => { ++count; return false; }, trans, coreTable) + .then(() => count); + } + }).then(cb); + } + sortBy(keyPath, cb) { + const parts = keyPath.split('.').reverse(), lastPart = parts[0], lastIndex = parts.length - 1; + function getval(obj, i) { + if (i) + return getval(obj[parts[i]], i - 1); + return obj[lastPart]; + } + var order = this._ctx.dir === "next" ? 1 : -1; + function sorter(a, b) { + var aVal = getval(a, lastIndex), bVal = getval(b, lastIndex); + return cmp(aVal, bVal) * order; + } + return this.toArray(function (a) { + return a.sort(sorter); + }).then(cb); + } + toArray(cb) { + return this._read(trans => { + var ctx = this._ctx; + if (ctx.dir === 'next' && isPlainKeyRange(ctx, true) && ctx.limit > 0) { + const { valueMapper } = ctx; + const index = getIndexOrStore(ctx, ctx.table.core.schema); + return ctx.table.core.query({ + trans, + limit: ctx.limit, + values: true, + query: { + index, + range: ctx.range + } + }).then(({ result }) => valueMapper ? result.map(valueMapper) : result); + } + else { + const a = []; + return iter(ctx, item => a.push(item), trans, ctx.table.core).then(() => a); + } + }, cb); + } + offset(offset) { + var ctx = this._ctx; + if (offset <= 0) + return this; + ctx.offset += offset; + if (isPlainKeyRange(ctx)) { + addReplayFilter(ctx, () => { + var offsetLeft = offset; + return (cursor, advance) => { + if (offsetLeft === 0) + return true; + if (offsetLeft === 1) { + --offsetLeft; + return false; + } + advance(() => { + cursor.advance(offsetLeft); + offsetLeft = 0; + }); + return false; + }; + }); + } + else { + addReplayFilter(ctx, () => { + var offsetLeft = offset; + return () => (--offsetLeft < 0); + }); + } + return this; + } + limit(numRows) { + this._ctx.limit = Math.min(this._ctx.limit, numRows); + addReplayFilter(this._ctx, () => { + var rowsLeft = numRows; + return function (cursor, advance, resolve) { + if (--rowsLeft <= 0) + advance(resolve); + return rowsLeft >= 0; + }; + }, true); + return this; + } + until(filterFunction, bIncludeStopEntry) { + addFilter(this._ctx, function (cursor, advance, resolve) { + if (filterFunction(cursor.value)) { + advance(resolve); + return bIncludeStopEntry; + } + else { + return true; + } + }); + return this; + } + first(cb) { + return this.limit(1).toArray(function (a) { return a[0]; }).then(cb); + } + last(cb) { + return this.reverse().first(cb); + } + filter(filterFunction) { + addFilter(this._ctx, function (cursor) { + return filterFunction(cursor.value); + }); + addMatchFilter(this._ctx, filterFunction); + return this; + } + and(filter) { + return this.filter(filter); + } + or(indexName) { + return new this.db.WhereClause(this._ctx.table, indexName, this); + } + reverse() { + this._ctx.dir = (this._ctx.dir === "prev" ? "next" : "prev"); + if (this._ondirectionchange) + this._ondirectionchange(this._ctx.dir); + return this; + } + desc() { + return this.reverse(); + } + eachKey(cb) { + var ctx = this._ctx; + ctx.keysOnly = !ctx.isMatch; + return this.each(function (val, cursor) { cb(cursor.key, cursor); }); + } + eachUniqueKey(cb) { + this._ctx.unique = "unique"; + return this.eachKey(cb); + } + eachPrimaryKey(cb) { + var ctx = this._ctx; + ctx.keysOnly = !ctx.isMatch; + return this.each(function (val, cursor) { cb(cursor.primaryKey, cursor); }); + } + keys(cb) { + var ctx = this._ctx; + ctx.keysOnly = !ctx.isMatch; + var a = []; + return this.each(function (item, cursor) { + a.push(cursor.key); + }).then(function () { + return a; + }).then(cb); + } + primaryKeys(cb) { + var ctx = this._ctx; + if (ctx.dir === 'next' && isPlainKeyRange(ctx, true) && ctx.limit > 0) { + return this._read(trans => { + var index = getIndexOrStore(ctx, ctx.table.core.schema); + return ctx.table.core.query({ + trans, + values: false, + limit: ctx.limit, + query: { + index, + range: ctx.range + } + }); + }).then(({ result }) => result).then(cb); + } + ctx.keysOnly = !ctx.isMatch; + var a = []; + return this.each(function (item, cursor) { + a.push(cursor.primaryKey); + }).then(function () { + return a; + }).then(cb); + } + uniqueKeys(cb) { + this._ctx.unique = "unique"; + return this.keys(cb); + } + firstKey(cb) { + return this.limit(1).keys(function (a) { return a[0]; }).then(cb); + } + lastKey(cb) { + return this.reverse().firstKey(cb); + } + distinct() { + var ctx = this._ctx, idx = ctx.index && ctx.table.schema.idxByName[ctx.index]; + if (!idx || !idx.multi) + return this; + var set = {}; + addFilter(this._ctx, function (cursor) { + var strKey = cursor.primaryKey.toString(); + var found = hasOwn(set, strKey); + set[strKey] = true; + return !found; + }); + return this; + } + modify(changes) { + var ctx = this._ctx; + return this._write(trans => { + var modifyer; + if (typeof changes === 'function') { + modifyer = changes; + } + else { + modifyer = item => applyUpdateSpec(item, changes); + } + const coreTable = ctx.table.core; + const { outbound, extractKey } = coreTable.schema.primaryKey; + let limit = 200; + const modifyChunkSize = this.db._options.modifyChunkSize; + if (modifyChunkSize) { + if (typeof modifyChunkSize == 'object') { + limit = modifyChunkSize[coreTable.name] || modifyChunkSize['*'] || 200; + } + else { + limit = modifyChunkSize; + } + } + const totalFailures = []; + let successCount = 0; + const failedKeys = []; + const applyMutateResult = (expectedCount, res) => { + const { failures, numFailures } = res; + successCount += expectedCount - numFailures; + for (let pos of keys(failures)) { + totalFailures.push(failures[pos]); + } + }; + const isUnconditionalDelete = changes === deleteCallback; + return this.clone().primaryKeys().then(keys => { + const criteria = isPlainKeyRange(ctx) && + ctx.limit === Infinity && + (typeof changes !== 'function' || isUnconditionalDelete) && { + index: ctx.index, + range: ctx.range + }; + const nextChunk = (offset) => { + const count = Math.min(limit, keys.length - offset); + const keysInChunk = keys.slice(offset, offset + count); + return (isUnconditionalDelete ? Promise.resolve([]) : coreTable.getMany({ + trans, + keys: keysInChunk, + cache: "immutable" + })).then(values => { + const addValues = []; + const putValues = []; + const putKeys = outbound ? [] : null; + const deleteKeys = isUnconditionalDelete ? keysInChunk : []; + if (!isUnconditionalDelete) + for (let i = 0; i < count; ++i) { + const origValue = values[i]; + const ctx = { + value: deepClone(origValue), + primKey: keys[offset + i] + }; + if (modifyer.call(ctx, ctx.value, ctx) !== false) { + if (ctx.value == null) { + deleteKeys.push(keys[offset + i]); + } + else if (!outbound && cmp(extractKey(origValue), extractKey(ctx.value)) !== 0) { + deleteKeys.push(keys[offset + i]); + addValues.push(ctx.value); + } + else { + putValues.push(ctx.value); + if (outbound) + putKeys.push(keys[offset + i]); + } + } + } + return Promise.resolve(addValues.length > 0 && + coreTable.mutate({ trans, type: 'add', values: addValues }) + .then(res => { + for (let pos in res.failures) { + deleteKeys.splice(parseInt(pos), 1); + } + applyMutateResult(addValues.length, res); + })).then(() => (putValues.length > 0 || (criteria && typeof changes === 'object')) && + coreTable.mutate({ + trans, + type: 'put', + keys: putKeys, + values: putValues, + criteria, + changeSpec: typeof changes !== 'function' + && changes, + isAdditionalChunk: offset > 0 + }).then(res => applyMutateResult(putValues.length, res))).then(() => (deleteKeys.length > 0 || (criteria && isUnconditionalDelete)) && + coreTable.mutate({ + trans, + type: 'delete', + keys: deleteKeys, + criteria, + isAdditionalChunk: offset > 0 + }).then(res => builtInDeletionTrigger(ctx.table, deleteKeys, res)) + .then(res => applyMutateResult(deleteKeys.length, res))).then(() => { + return keys.length > offset + count && nextChunk(offset + limit); + }); + }); + }; + return nextChunk(0).then(() => { + if (totalFailures.length > 0) + throw new ModifyError("Error modifying one or more objects", totalFailures, successCount, failedKeys); + return keys.length; + }); + }); + }); + } + delete() { + var ctx = this._ctx, range = ctx.range; + if (isPlainKeyRange(ctx) && + !ctx.table.schema.yProps && + (ctx.isPrimKey || range.type === 3 )) + { + return this._write(trans => { + const { primaryKey } = ctx.table.core.schema; + const coreRange = range; + return ctx.table.core.count({ trans, query: { index: primaryKey, range: coreRange } }).then(count => { + return ctx.table.core.mutate({ trans, type: 'deleteRange', range: coreRange }) + .then(({ failures, numFailures }) => { + if (numFailures) + throw new ModifyError("Could not delete some values", Object.keys(failures).map(pos => failures[pos]), count - numFailures); + return count - numFailures; + }); + }); + }); + } + return this.modify(deleteCallback); + } +} +const deleteCallback = (value, ctx) => ctx.value = null; + +function createCollectionConstructor(db) { + return makeClassConstructor(Collection.prototype, function Collection(whereClause, keyRangeGenerator) { + this.db = db; + let keyRange = AnyRange, error = null; + if (keyRangeGenerator) + try { + keyRange = keyRangeGenerator(); + } + catch (ex) { + error = ex; + } + const whereCtx = whereClause._ctx; + const table = whereCtx.table; + const readingHook = table.hook.reading.fire; + this._ctx = { + table: table, + index: whereCtx.index, + isPrimKey: (!whereCtx.index || (table.schema.primKey.keyPath && whereCtx.index === table.schema.primKey.name)), + range: keyRange, + keysOnly: false, + dir: "next", + unique: "", + algorithm: null, + filter: null, + replayFilter: null, + justLimit: true, + isMatch: null, + offset: 0, + limit: Infinity, + error: error, + or: whereCtx.or, + valueMapper: readingHook !== mirror ? readingHook : null + }; + }); +} + +function simpleCompare(a, b) { + return a < b ? -1 : a === b ? 0 : 1; +} +function simpleCompareReverse(a, b) { + return a > b ? -1 : a === b ? 0 : 1; +} + +function fail(collectionOrWhereClause, err, T) { + var collection = collectionOrWhereClause instanceof WhereClause ? + new collectionOrWhereClause.Collection(collectionOrWhereClause) : + collectionOrWhereClause; + collection._ctx.error = T ? new T(err) : new TypeError(err); + return collection; +} +function emptyCollection(whereClause) { + return new whereClause.Collection(whereClause, () => rangeEqual("")).limit(0); +} +function upperFactory(dir) { + return dir === "next" ? + (s) => s.toUpperCase() : + (s) => s.toLowerCase(); +} +function lowerFactory(dir) { + return dir === "next" ? + (s) => s.toLowerCase() : + (s) => s.toUpperCase(); +} +function nextCasing(key, lowerKey, upperNeedle, lowerNeedle, cmp, dir) { + var length = Math.min(key.length, lowerNeedle.length); + var llp = -1; + for (var i = 0; i < length; ++i) { + var lwrKeyChar = lowerKey[i]; + if (lwrKeyChar !== lowerNeedle[i]) { + if (cmp(key[i], upperNeedle[i]) < 0) + return key.substr(0, i) + upperNeedle[i] + upperNeedle.substr(i + 1); + if (cmp(key[i], lowerNeedle[i]) < 0) + return key.substr(0, i) + lowerNeedle[i] + upperNeedle.substr(i + 1); + if (llp >= 0) + return key.substr(0, llp) + lowerKey[llp] + upperNeedle.substr(llp + 1); + return null; + } + if (cmp(key[i], lwrKeyChar) < 0) + llp = i; + } + if (length < lowerNeedle.length && dir === "next") + return key + upperNeedle.substr(key.length); + if (length < key.length && dir === "prev") + return key.substr(0, upperNeedle.length); + return (llp < 0 ? null : key.substr(0, llp) + lowerNeedle[llp] + upperNeedle.substr(llp + 1)); +} +function addIgnoreCaseAlgorithm(whereClause, match, needles, suffix) { + var upper, lower, compare, upperNeedles, lowerNeedles, direction, nextKeySuffix, needlesLen = needles.length; + if (!needles.every(s => typeof s === 'string')) { + return fail(whereClause, STRING_EXPECTED); + } + function initDirection(dir) { + upper = upperFactory(dir); + lower = lowerFactory(dir); + compare = (dir === "next" ? simpleCompare : simpleCompareReverse); + var needleBounds = needles.map(function (needle) { + return { lower: lower(needle), upper: upper(needle) }; + }).sort(function (a, b) { + return compare(a.lower, b.lower); + }); + upperNeedles = needleBounds.map(function (nb) { return nb.upper; }); + lowerNeedles = needleBounds.map(function (nb) { return nb.lower; }); + direction = dir; + nextKeySuffix = (dir === "next" ? "" : suffix); + } + initDirection("next"); + var c = new whereClause.Collection(whereClause, () => createRange(upperNeedles[0], lowerNeedles[needlesLen - 1] + suffix)); + c._ondirectionchange = function (direction) { + initDirection(direction); + }; + var firstPossibleNeedle = 0; + c._addAlgorithm(function (cursor, advance, resolve) { + var key = cursor.key; + if (typeof key !== 'string') + return false; + var lowerKey = lower(key); + if (match(lowerKey, lowerNeedles, firstPossibleNeedle)) { + return true; + } + else { + var lowestPossibleCasing = null; + for (var i = firstPossibleNeedle; i < needlesLen; ++i) { + var casing = nextCasing(key, lowerKey, upperNeedles[i], lowerNeedles[i], compare, direction); + if (casing === null && lowestPossibleCasing === null) + firstPossibleNeedle = i + 1; + else if (lowestPossibleCasing === null || compare(lowestPossibleCasing, casing) > 0) { + lowestPossibleCasing = casing; + } + } + if (lowestPossibleCasing !== null) { + advance(function () { cursor.continue(lowestPossibleCasing + nextKeySuffix); }); + } + else { + advance(resolve); + } + return false; + } + }); + return c; +} +function createRange(lower, upper, lowerOpen, upperOpen) { + return { + type: 2 , + lower, + upper, + lowerOpen, + upperOpen + }; +} +function rangeEqual(value) { + return { + type: 1 , + lower: value, + upper: value + }; +} + +class WhereClause { + get Collection() { + return this._ctx.table.db.Collection; + } + between(lower, upper, includeLower, includeUpper) { + includeLower = includeLower !== false; + includeUpper = includeUpper === true; + try { + if ((this._cmp(lower, upper) > 0) || + (this._cmp(lower, upper) === 0 && (includeLower || includeUpper) && !(includeLower && includeUpper))) + return emptyCollection(this); + return new this.Collection(this, () => createRange(lower, upper, !includeLower, !includeUpper)); + } + catch (e) { + return fail(this, INVALID_KEY_ARGUMENT); + } + } + equals(value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, () => rangeEqual(value)); + } + above(value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, () => createRange(value, undefined, true)); + } + aboveOrEqual(value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, () => createRange(value, undefined, false)); + } + below(value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, () => createRange(undefined, value, false, true)); + } + belowOrEqual(value) { + if (value == null) + return fail(this, INVALID_KEY_ARGUMENT); + return new this.Collection(this, () => createRange(undefined, value)); + } + startsWith(str) { + if (typeof str !== 'string') + return fail(this, STRING_EXPECTED); + return this.between(str, str + maxString, true, true); + } + startsWithIgnoreCase(str) { + if (str === "") + return this.startsWith(str); + return addIgnoreCaseAlgorithm(this, (x, a) => x.indexOf(a[0]) === 0, [str], maxString); + } + equalsIgnoreCase(str) { + return addIgnoreCaseAlgorithm(this, (x, a) => x === a[0], [str], ""); + } + anyOfIgnoreCase() { + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (set.length === 0) + return emptyCollection(this); + return addIgnoreCaseAlgorithm(this, (x, a) => a.indexOf(x) !== -1, set, ""); + } + startsWithAnyOfIgnoreCase() { + var set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (set.length === 0) + return emptyCollection(this); + return addIgnoreCaseAlgorithm(this, (x, a) => a.some(n => x.indexOf(n) === 0), set, maxString); + } + anyOf() { + const set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + let compare = this._cmp; + try { + set.sort(compare); + } + catch (e) { + return fail(this, INVALID_KEY_ARGUMENT); + } + if (set.length === 0) + return emptyCollection(this); + const c = new this.Collection(this, () => createRange(set[0], set[set.length - 1])); + c._ondirectionchange = direction => { + compare = (direction === "next" ? + this._ascending : + this._descending); + set.sort(compare); + }; + let i = 0; + c._addAlgorithm((cursor, advance, resolve) => { + const key = cursor.key; + while (compare(key, set[i]) > 0) { + ++i; + if (i === set.length) { + advance(resolve); + return false; + } + } + if (compare(key, set[i]) === 0) { + return true; + } + else { + advance(() => { cursor.continue(set[i]); }); + return false; + } + }); + return c; + } + notEqual(value) { + return this.inAnyRange([[minKey, value], [value, this.db._maxKey]], { includeLowers: false, includeUppers: false }); + } + noneOf() { + const set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (set.length === 0) + return new this.Collection(this); + try { + set.sort(this._ascending); + } + catch (e) { + return fail(this, INVALID_KEY_ARGUMENT); + } + const ranges = set.reduce((res, val) => res ? + res.concat([[res[res.length - 1][1], val]]) : + [[minKey, val]], null); + ranges.push([set[set.length - 1], this.db._maxKey]); + return this.inAnyRange(ranges, { includeLowers: false, includeUppers: false }); + } + inAnyRange(ranges, options) { + const cmp = this._cmp, ascending = this._ascending, descending = this._descending, min = this._min, max = this._max; + if (ranges.length === 0) + return emptyCollection(this); + if (!ranges.every(range => range[0] !== undefined && + range[1] !== undefined && + ascending(range[0], range[1]) <= 0)) { + return fail(this, "First argument to inAnyRange() must be an Array of two-value Arrays [lower,upper] where upper must not be lower than lower", exceptions.InvalidArgument); + } + const includeLowers = !options || options.includeLowers !== false; + const includeUppers = options && options.includeUppers === true; + function addRange(ranges, newRange) { + let i = 0, l = ranges.length; + for (; i < l; ++i) { + const range = ranges[i]; + if (cmp(newRange[0], range[1]) < 0 && cmp(newRange[1], range[0]) > 0) { + range[0] = min(range[0], newRange[0]); + range[1] = max(range[1], newRange[1]); + break; + } + } + if (i === l) + ranges.push(newRange); + return ranges; + } + let sortDirection = ascending; + function rangeSorter(a, b) { return sortDirection(a[0], b[0]); } + let set; + try { + set = ranges.reduce(addRange, []); + set.sort(rangeSorter); + } + catch (ex) { + return fail(this, INVALID_KEY_ARGUMENT); + } + let rangePos = 0; + const keyIsBeyondCurrentEntry = includeUppers ? + key => ascending(key, set[rangePos][1]) > 0 : + key => ascending(key, set[rangePos][1]) >= 0; + const keyIsBeforeCurrentEntry = includeLowers ? + key => descending(key, set[rangePos][0]) > 0 : + key => descending(key, set[rangePos][0]) >= 0; + function keyWithinCurrentRange(key) { + return !keyIsBeyondCurrentEntry(key) && !keyIsBeforeCurrentEntry(key); + } + let checkKey = keyIsBeyondCurrentEntry; + const c = new this.Collection(this, () => createRange(set[0][0], set[set.length - 1][1], !includeLowers, !includeUppers)); + c._ondirectionchange = direction => { + if (direction === "next") { + checkKey = keyIsBeyondCurrentEntry; + sortDirection = ascending; + } + else { + checkKey = keyIsBeforeCurrentEntry; + sortDirection = descending; + } + set.sort(rangeSorter); + }; + c._addAlgorithm((cursor, advance, resolve) => { + var key = cursor.key; + while (checkKey(key)) { + ++rangePos; + if (rangePos === set.length) { + advance(resolve); + return false; + } + } + if (keyWithinCurrentRange(key)) { + return true; + } + else if (this._cmp(key, set[rangePos][1]) === 0 || this._cmp(key, set[rangePos][0]) === 0) { + return false; + } + else { + advance(() => { + if (sortDirection === ascending) + cursor.continue(set[rangePos][0]); + else + cursor.continue(set[rangePos][1]); + }); + return false; + } + }); + return c; + } + startsWithAnyOf() { + const set = getArrayOf.apply(NO_CHAR_ARRAY, arguments); + if (!set.every(s => typeof s === 'string')) { + return fail(this, "startsWithAnyOf() only works with strings"); + } + if (set.length === 0) + return emptyCollection(this); + return this.inAnyRange(set.map((str) => [str, str + maxString])); + } +} + +function createWhereClauseConstructor(db) { + return makeClassConstructor(WhereClause.prototype, function WhereClause(table, index, orCollection) { + this.db = db; + this._ctx = { + table: table, + index: index === ":id" ? null : index, + or: orCollection + }; + this._cmp = this._ascending = cmp; + this._descending = (a, b) => cmp(b, a); + this._max = (a, b) => cmp(a, b) > 0 ? a : b; + this._min = (a, b) => cmp(a, b) < 0 ? a : b; + this._IDBKeyRange = db._deps.IDBKeyRange; + if (!this._IDBKeyRange) + throw new exceptions.MissingAPI(); + }); +} + +function eventRejectHandler(reject) { + return wrap(function (event) { + preventDefault(event); + reject(event.target.error); + return false; + }); +} +function preventDefault(event) { + if (event.stopPropagation) + event.stopPropagation(); + if (event.preventDefault) + event.preventDefault(); +} + +const DEXIE_STORAGE_MUTATED_EVENT_NAME = 'storagemutated'; +const STORAGE_MUTATED_DOM_EVENT_NAME = 'x-storagemutated-1'; +const globalEvents = Events(null, DEXIE_STORAGE_MUTATED_EVENT_NAME); + +class Transaction { + _lock() { + assert(!PSD.global); + ++this._reculock; + if (this._reculock === 1 && !PSD.global) + PSD.lockOwnerFor = this; + return this; + } + _unlock() { + assert(!PSD.global); + if (--this._reculock === 0) { + if (!PSD.global) + PSD.lockOwnerFor = null; + while (this._blockedFuncs.length > 0 && !this._locked()) { + var fnAndPSD = this._blockedFuncs.shift(); + try { + usePSD(fnAndPSD[1], fnAndPSD[0]); + } + catch (e) { } + } + } + return this; + } + _locked() { + return this._reculock && PSD.lockOwnerFor !== this; + } + create(idbtrans) { + if (!this.mode) + return this; + const idbdb = this.db.idbdb; + const dbOpenError = this.db._state.dbOpenError; + assert(!this.idbtrans); + if (!idbtrans && !idbdb) { + switch (dbOpenError && dbOpenError.name) { + case "DatabaseClosedError": + throw new exceptions.DatabaseClosed(dbOpenError); + case "MissingAPIError": + throw new exceptions.MissingAPI(dbOpenError.message, dbOpenError); + default: + throw new exceptions.OpenFailed(dbOpenError); + } + } + if (!this.active) + throw new exceptions.TransactionInactive(); + assert(this._completion._state === null); + idbtrans = this.idbtrans = idbtrans || + (this.db.core + ? this.db.core.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability }) + : idbdb.transaction(this.storeNames, this.mode, { durability: this.chromeTransactionDurability })); + idbtrans.onerror = wrap(ev => { + preventDefault(ev); + this._reject(idbtrans.error); + }); + idbtrans.onabort = wrap(ev => { + preventDefault(ev); + this.active && this._reject(new exceptions.Abort(idbtrans.error)); + this.active = false; + this.on("abort").fire(ev); + }); + idbtrans.oncomplete = wrap(() => { + this.active = false; + this._resolve(); + if ('mutatedParts' in idbtrans) { + globalEvents.storagemutated.fire(idbtrans["mutatedParts"]); + } + }); + return this; + } + _promise(mode, fn, bWriteLock) { + if (mode === 'readwrite' && this.mode !== 'readwrite') + return rejection(new exceptions.ReadOnly("Transaction is readonly")); + if (!this.active) + return rejection(new exceptions.TransactionInactive()); + if (this._locked()) { + return new DexiePromise((resolve, reject) => { + this._blockedFuncs.push([() => { + this._promise(mode, fn, bWriteLock).then(resolve, reject); + }, PSD]); + }); + } + else if (bWriteLock) { + return newScope(() => { + var p = new DexiePromise((resolve, reject) => { + this._lock(); + const rv = fn(resolve, reject, this); + if (rv && rv.then) + rv.then(resolve, reject); + }); + p.finally(() => this._unlock()); + p._lib = true; + return p; + }); + } + else { + var p = new DexiePromise((resolve, reject) => { + var rv = fn(resolve, reject, this); + if (rv && rv.then) + rv.then(resolve, reject); + }); + p._lib = true; + return p; + } + } + _root() { + return this.parent ? this.parent._root() : this; + } + waitFor(promiseLike) { + var root = this._root(); + const promise = DexiePromise.resolve(promiseLike); + if (root._waitingFor) { + root._waitingFor = root._waitingFor.then(() => promise); + } + else { + root._waitingFor = promise; + root._waitingQueue = []; + var store = root.idbtrans.objectStore(root.storeNames[0]); + (function spin() { + ++root._spinCount; + while (root._waitingQueue.length) + (root._waitingQueue.shift())(); + if (root._waitingFor) + store.get(-Infinity).onsuccess = spin; + }()); + } + var currentWaitPromise = root._waitingFor; + return new DexiePromise((resolve, reject) => { + promise.then(res => root._waitingQueue.push(wrap(resolve.bind(null, res))), err => root._waitingQueue.push(wrap(reject.bind(null, err)))).finally(() => { + if (root._waitingFor === currentWaitPromise) { + root._waitingFor = null; + } + }); + }); + } + abort() { + if (this.active) { + this.active = false; + if (this.idbtrans) + this.idbtrans.abort(); + this._reject(new exceptions.Abort()); + } + } + table(tableName) { + const memoizedTables = (this._memoizedTables || (this._memoizedTables = {})); + if (hasOwn(memoizedTables, tableName)) + return memoizedTables[tableName]; + const tableSchema = this.schema[tableName]; + if (!tableSchema) { + throw new exceptions.NotFound("Table " + tableName + " not part of transaction"); + } + const transactionBoundTable = new this.db.Table(tableName, tableSchema, this); + transactionBoundTable.core = this.db.core.table(tableName); + memoizedTables[tableName] = transactionBoundTable; + return transactionBoundTable; + } +} + +function createTransactionConstructor(db) { + return makeClassConstructor(Transaction.prototype, function Transaction(mode, storeNames, dbschema, chromeTransactionDurability, parent) { + if (mode !== 'readonly') + storeNames.forEach(storeName => { + const yProps = dbschema[storeName]?.yProps; + if (yProps) + storeNames = storeNames.concat(yProps.map(p => p.updatesTable)); + }); + this.db = db; + this.mode = mode; + this.storeNames = storeNames; + this.schema = dbschema; + this.chromeTransactionDurability = chromeTransactionDurability; + this.idbtrans = null; + this.on = Events(this, "complete", "error", "abort"); + this.parent = parent || null; + this.active = true; + this._reculock = 0; + this._blockedFuncs = []; + this._resolve = null; + this._reject = null; + this._waitingFor = null; + this._waitingQueue = null; + this._spinCount = 0; + this._completion = new DexiePromise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + this._completion.then(() => { + this.active = false; + this.on.complete.fire(); + }, e => { + var wasActive = this.active; + this.active = false; + this.on.error.fire(e); + this.parent ? + this.parent._reject(e) : + wasActive && this.idbtrans && this.idbtrans.abort(); + return rejection(e); + }); + }); +} + +function createIndexSpec(name, keyPath, unique, multi, auto, compound, isPrimKey, type) { + return { + name, + keyPath, + unique, + multi, + auto, + compound, + src: (unique && !isPrimKey ? '&' : '') + (multi ? '*' : '') + (auto ? "++" : "") + nameFromKeyPath(keyPath), + type + }; +} +function nameFromKeyPath(keyPath) { + return typeof keyPath === 'string' ? + keyPath : + keyPath ? ('[' + [].join.call(keyPath, '+') + ']') : ""; +} + +function createTableSchema(name, primKey, indexes) { + return { + name, + primKey, + indexes, + mappedClass: null, + idxByName: arrayToObject(indexes, (index) => [index.name, index]), + }; +} + +function safariMultiStoreFix(storeNames) { + return storeNames.length === 1 ? storeNames[0] : storeNames; +} +let getMaxKey = (IdbKeyRange) => { + try { + IdbKeyRange.only([[]]); + getMaxKey = () => [[]]; + return [[]]; + } + catch (e) { + getMaxKey = () => maxString; + return maxString; + } +}; + +function getKeyExtractor(keyPath) { + if (keyPath == null) { + return () => undefined; + } + else if (typeof keyPath === 'string') { + return getSinglePathKeyExtractor(keyPath); + } + else { + return obj => getByKeyPath(obj, keyPath); + } +} +function getSinglePathKeyExtractor(keyPath) { + const split = keyPath.split('.'); + if (split.length === 1) { + return obj => obj[keyPath]; + } + else { + return obj => getByKeyPath(obj, keyPath); + } +} + +function arrayify(arrayLike) { + return [].slice.call(arrayLike); +} +let _id_counter = 0; +function getKeyPathAlias(keyPath) { + return keyPath == null ? + ":id" : + typeof keyPath === 'string' ? + keyPath : + `[${keyPath.join('+')}]`; +} +function createDBCore(db, IdbKeyRange, tmpTrans) { + function extractSchema(db, trans) { + const tables = arrayify(db.objectStoreNames); + return { + schema: { + name: db.name, + tables: tables.map(table => trans.objectStore(table)).map(store => { + const { keyPath, autoIncrement } = store; + const compound = isArray(keyPath); + const outbound = keyPath == null; + const indexByKeyPath = {}; + const result = { + name: store.name, + primaryKey: { + name: null, + isPrimaryKey: true, + outbound, + compound, + keyPath, + autoIncrement, + unique: true, + extractKey: getKeyExtractor(keyPath) + }, + indexes: arrayify(store.indexNames).map(indexName => store.index(indexName)) + .map(index => { + const { name, unique, multiEntry, keyPath } = index; + const compound = isArray(keyPath); + const result = { + name, + compound, + keyPath, + unique, + multiEntry, + extractKey: getKeyExtractor(keyPath) + }; + indexByKeyPath[getKeyPathAlias(keyPath)] = result; + return result; + }), + getIndexByKeyPath: (keyPath) => indexByKeyPath[getKeyPathAlias(keyPath)] + }; + indexByKeyPath[":id"] = result.primaryKey; + if (keyPath != null) { + indexByKeyPath[getKeyPathAlias(keyPath)] = result.primaryKey; + } + return result; + }) + }, + hasGetAll: tables.length > 0 && ('getAll' in trans.objectStore(tables[0])) && + !(typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) && + !/(Chrome\/|Edge\/)/.test(navigator.userAgent) && + [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604) + }; + } + function makeIDBKeyRange(range) { + if (range.type === 3 ) + return null; + if (range.type === 4 ) + throw new Error("Cannot convert never type to IDBKeyRange"); + const { lower, upper, lowerOpen, upperOpen } = range; + const idbRange = lower === undefined ? + upper === undefined ? + null : + IdbKeyRange.upperBound(upper, !!upperOpen) : + upper === undefined ? + IdbKeyRange.lowerBound(lower, !!lowerOpen) : + IdbKeyRange.bound(lower, upper, !!lowerOpen, !!upperOpen); + return idbRange; + } + function createDbCoreTable(tableSchema) { + const tableName = tableSchema.name; + function mutate({ trans, type, keys, values, range }) { + return new Promise((resolve, reject) => { + resolve = wrap(resolve); + const store = trans.objectStore(tableName); + const outbound = store.keyPath == null; + const isAddOrPut = type === "put" || type === "add"; + if (!isAddOrPut && type !== 'delete' && type !== 'deleteRange') + throw new Error("Invalid operation type: " + type); + const { length } = keys || values || { length: 1 }; + if (keys && values && keys.length !== values.length) { + throw new Error("Given keys array must have same length as given values array."); + } + if (length === 0) + return resolve({ numFailures: 0, failures: {}, results: [], lastResult: undefined }); + let req; + const reqs = []; + const failures = []; + let numFailures = 0; + const errorHandler = event => { + ++numFailures; + preventDefault(event); + }; + if (type === 'deleteRange') { + if (range.type === 4 ) + return resolve({ numFailures, failures, results: [], lastResult: undefined }); + if (range.type === 3 ) + reqs.push(req = store.clear()); + else + reqs.push(req = store.delete(makeIDBKeyRange(range))); + } + else { + const [args1, args2] = isAddOrPut ? + outbound ? + [values, keys] : + [values, null] : + [keys, null]; + if (isAddOrPut) { + for (let i = 0; i < length; ++i) { + reqs.push(req = (args2 && args2[i] !== undefined ? + store[type](args1[i], args2[i]) : + store[type](args1[i]))); + req.onerror = errorHandler; + } + } + else { + for (let i = 0; i < length; ++i) { + reqs.push(req = store[type](args1[i])); + req.onerror = errorHandler; + } + } + } + const done = event => { + const lastResult = event.target.result; + reqs.forEach((req, i) => req.error != null && (failures[i] = req.error)); + resolve({ + numFailures, + failures, + results: type === "delete" ? keys : reqs.map(req => req.result), + lastResult + }); + }; + req.onerror = event => { + errorHandler(event); + done(event); + }; + req.onsuccess = done; + }); + } + function openCursor({ trans, values, query, reverse, unique }) { + return new Promise((resolve, reject) => { + resolve = wrap(resolve); + const { index, range } = query; + const store = trans.objectStore(tableName); + const source = index.isPrimaryKey ? + store : + store.index(index.name); + const direction = reverse ? + unique ? + "prevunique" : + "prev" : + unique ? + "nextunique" : + "next"; + const req = values || !('openKeyCursor' in source) ? + source.openCursor(makeIDBKeyRange(range), direction) : + source.openKeyCursor(makeIDBKeyRange(range), direction); + req.onerror = eventRejectHandler(reject); + req.onsuccess = wrap(ev => { + const cursor = req.result; + if (!cursor) { + resolve(null); + return; + } + cursor.___id = ++_id_counter; + cursor.done = false; + const _cursorContinue = cursor.continue.bind(cursor); + let _cursorContinuePrimaryKey = cursor.continuePrimaryKey; + if (_cursorContinuePrimaryKey) + _cursorContinuePrimaryKey = _cursorContinuePrimaryKey.bind(cursor); + const _cursorAdvance = cursor.advance.bind(cursor); + const doThrowCursorIsNotStarted = () => { throw new Error("Cursor not started"); }; + const doThrowCursorIsStopped = () => { throw new Error("Cursor not stopped"); }; + cursor.trans = trans; + cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsNotStarted; + cursor.fail = wrap(reject); + cursor.next = function () { + let gotOne = 1; + return this.start(() => gotOne-- ? this.continue() : this.stop()).then(() => this); + }; + cursor.start = (callback) => { + const iterationPromise = new Promise((resolveIteration, rejectIteration) => { + resolveIteration = wrap(resolveIteration); + req.onerror = eventRejectHandler(rejectIteration); + cursor.fail = rejectIteration; + cursor.stop = value => { + cursor.stop = cursor.continue = cursor.continuePrimaryKey = cursor.advance = doThrowCursorIsStopped; + resolveIteration(value); + }; + }); + const guardedCallback = () => { + if (req.result) { + try { + callback(); + } + catch (err) { + cursor.fail(err); + } + } + else { + cursor.done = true; + cursor.start = () => { throw new Error("Cursor behind last entry"); }; + cursor.stop(); + } + }; + req.onsuccess = wrap(ev => { + req.onsuccess = guardedCallback; + guardedCallback(); + }); + cursor.continue = _cursorContinue; + cursor.continuePrimaryKey = _cursorContinuePrimaryKey; + cursor.advance = _cursorAdvance; + guardedCallback(); + return iterationPromise; + }; + resolve(cursor); + }, reject); + }); + } + function query(hasGetAll) { + return (request) => { + return new Promise((resolve, reject) => { + resolve = wrap(resolve); + const { trans, values, limit, query } = request; + const nonInfinitLimit = limit === Infinity ? undefined : limit; + const { index, range } = query; + const store = trans.objectStore(tableName); + const source = index.isPrimaryKey ? store : store.index(index.name); + const idbKeyRange = makeIDBKeyRange(range); + if (limit === 0) + return resolve({ result: [] }); + if (hasGetAll) { + const req = values ? + source.getAll(idbKeyRange, nonInfinitLimit) : + source.getAllKeys(idbKeyRange, nonInfinitLimit); + req.onsuccess = event => resolve({ result: event.target.result }); + req.onerror = eventRejectHandler(reject); + } + else { + let count = 0; + const req = values || !('openKeyCursor' in source) ? + source.openCursor(idbKeyRange) : + source.openKeyCursor(idbKeyRange); + const result = []; + req.onsuccess = event => { + const cursor = req.result; + if (!cursor) + return resolve({ result }); + result.push(values ? cursor.value : cursor.primaryKey); + if (++count === limit) + return resolve({ result }); + cursor.continue(); + }; + req.onerror = eventRejectHandler(reject); + } + }); + }; + } + return { + name: tableName, + schema: tableSchema, + mutate, + getMany({ trans, keys }) { + return new Promise((resolve, reject) => { + resolve = wrap(resolve); + const store = trans.objectStore(tableName); + const length = keys.length; + const result = new Array(length); + let keyCount = 0; + let callbackCount = 0; + let req; + const successHandler = event => { + const req = event.target; + if ((result[req._pos] = req.result) != null) + ; + if (++callbackCount === keyCount) + resolve(result); + }; + const errorHandler = eventRejectHandler(reject); + for (let i = 0; i < length; ++i) { + const key = keys[i]; + if (key != null) { + req = store.get(keys[i]); + req._pos = i; + req.onsuccess = successHandler; + req.onerror = errorHandler; + ++keyCount; + } + } + if (keyCount === 0) + resolve(result); + }); + }, + get({ trans, key }) { + return new Promise((resolve, reject) => { + resolve = wrap(resolve); + const store = trans.objectStore(tableName); + const req = store.get(key); + req.onsuccess = event => resolve(event.target.result); + req.onerror = eventRejectHandler(reject); + }); + }, + query: query(hasGetAll), + openCursor, + count({ query, trans }) { + const { index, range } = query; + return new Promise((resolve, reject) => { + const store = trans.objectStore(tableName); + const source = index.isPrimaryKey ? store : store.index(index.name); + const idbKeyRange = makeIDBKeyRange(range); + const req = idbKeyRange ? source.count(idbKeyRange) : source.count(); + req.onsuccess = wrap(ev => resolve(ev.target.result)); + req.onerror = eventRejectHandler(reject); + }); + } + }; + } + const { schema, hasGetAll } = extractSchema(db, tmpTrans); + const tables = schema.tables.map(tableSchema => createDbCoreTable(tableSchema)); + const tableMap = {}; + tables.forEach(table => tableMap[table.name] = table); + return { + stack: "dbcore", + transaction: db.transaction.bind(db), + table(name) { + const result = tableMap[name]; + if (!result) + throw new Error(`Table '${name}' not found`); + return tableMap[name]; + }, + MIN_KEY: -Infinity, + MAX_KEY: getMaxKey(IdbKeyRange), + schema + }; +} + +function createMiddlewareStack(stackImpl, middlewares) { + return middlewares.reduce((down, { create }) => ({ ...down, ...create(down) }), stackImpl); +} +function createMiddlewareStacks(middlewares, idbdb, { IDBKeyRange, indexedDB }, tmpTrans) { + const dbcore = createMiddlewareStack(createDBCore(idbdb, IDBKeyRange, tmpTrans), middlewares.dbcore); + return { + dbcore + }; +} +function generateMiddlewareStacks(db, tmpTrans) { + const idbdb = tmpTrans.db; + const stacks = createMiddlewareStacks(db._middlewares, idbdb, db._deps, tmpTrans); + db.core = stacks.dbcore; + db.tables.forEach(table => { + const tableName = table.name; + if (db.core.schema.tables.some(tbl => tbl.name === tableName)) { + table.core = db.core.table(tableName); + if (db[tableName] instanceof db.Table) { + db[tableName].core = table.core; + } + } + }); +} + +function setApiOnPlace(db, objs, tableNames, dbschema) { + tableNames.forEach(tableName => { + const schema = dbschema[tableName]; + objs.forEach(obj => { + const propDesc = getPropertyDescriptor(obj, tableName); + if (!propDesc || ("value" in propDesc && propDesc.value === undefined)) { + if (obj === db.Transaction.prototype || obj instanceof db.Transaction) { + setProp(obj, tableName, { + get() { return this.table(tableName); }, + set(value) { + defineProperty(this, tableName, { value, writable: true, configurable: true, enumerable: true }); + } + }); + } + else { + obj[tableName] = new db.Table(tableName, schema); + } + } + }); + }); +} +function removeTablesApi(db, objs) { + objs.forEach(obj => { + for (let key in obj) { + if (obj[key] instanceof db.Table) + delete obj[key]; + } + }); +} +function lowerVersionFirst(a, b) { + return a._cfg.version - b._cfg.version; +} +function runUpgraders(db, oldVersion, idbUpgradeTrans, reject) { + const globalSchema = db._dbSchema; + if (idbUpgradeTrans.objectStoreNames.contains('$meta') && !globalSchema.$meta) { + globalSchema.$meta = createTableSchema("$meta", parseIndexSyntax("")[0], []); + db._storeNames.push('$meta'); + } + const trans = db._createTransaction('readwrite', db._storeNames, globalSchema); + trans.create(idbUpgradeTrans); + trans._completion.catch(reject); + const rejectTransaction = trans._reject.bind(trans); + const transless = PSD.transless || PSD; + newScope(() => { + PSD.trans = trans; + PSD.transless = transless; + if (oldVersion === 0) { + keys(globalSchema).forEach(tableName => { + createTable(idbUpgradeTrans, tableName, globalSchema[tableName].primKey, globalSchema[tableName].indexes); + }); + generateMiddlewareStacks(db, idbUpgradeTrans); + DexiePromise.follow(() => db.on.populate.fire(trans)).catch(rejectTransaction); + } + else { + generateMiddlewareStacks(db, idbUpgradeTrans); + return getExistingVersion(db, trans, oldVersion) + .then(oldVersion => updateTablesAndIndexes(db, oldVersion, trans, idbUpgradeTrans)) + .catch(rejectTransaction); + } + }); +} +function patchCurrentVersion(db, idbUpgradeTrans) { + createMissingTables(db._dbSchema, idbUpgradeTrans); + if (idbUpgradeTrans.db.version % 10 === 0 && !idbUpgradeTrans.objectStoreNames.contains('$meta')) { + idbUpgradeTrans.db.createObjectStore('$meta').add(Math.ceil((idbUpgradeTrans.db.version / 10) - 1), 'version'); + } + const globalSchema = buildGlobalSchema(db, db.idbdb, idbUpgradeTrans); + adjustToExistingIndexNames(db, db._dbSchema, idbUpgradeTrans); + const diff = getSchemaDiff(globalSchema, db._dbSchema); + for (const tableChange of diff.change) { + if (tableChange.change.length || tableChange.recreate) { + console.warn(`Unable to patch indexes of table ${tableChange.name} because it has changes on the type of index or primary key.`); + return; + } + const store = idbUpgradeTrans.objectStore(tableChange.name); + tableChange.add.forEach(idx => { + if (debug) + console.debug(`Dexie upgrade patch: Creating missing index ${tableChange.name}.${idx.src}`); + addIndex(store, idx); + }); + } +} +function getExistingVersion(db, trans, oldVersion) { + if (trans.storeNames.includes('$meta')) { + return trans.table('$meta').get('version').then(metaVersion => { + return metaVersion != null ? metaVersion : oldVersion; + }); + } + else { + return DexiePromise.resolve(oldVersion); + } +} +function updateTablesAndIndexes(db, oldVersion, trans, idbUpgradeTrans) { + const queue = []; + const versions = db._versions; + let globalSchema = db._dbSchema = buildGlobalSchema(db, db.idbdb, idbUpgradeTrans); + const versToRun = versions.filter(v => v._cfg.version >= oldVersion); + if (versToRun.length === 0) { + return DexiePromise.resolve(); + } + versToRun.forEach(version => { + queue.push(() => { + const oldSchema = globalSchema; + const newSchema = version._cfg.dbschema; + adjustToExistingIndexNames(db, oldSchema, idbUpgradeTrans); + adjustToExistingIndexNames(db, newSchema, idbUpgradeTrans); + globalSchema = db._dbSchema = newSchema; + const diff = getSchemaDiff(oldSchema, newSchema); + diff.add.forEach(tuple => { + createTable(idbUpgradeTrans, tuple[0], tuple[1].primKey, tuple[1].indexes); + }); + diff.change.forEach(change => { + if (change.recreate) { + throw new exceptions.Upgrade("Not yet support for changing primary key"); + } + else { + const store = idbUpgradeTrans.objectStore(change.name); + change.add.forEach(idx => addIndex(store, idx)); + change.change.forEach(idx => { + store.deleteIndex(idx.name); + addIndex(store, idx); + }); + change.del.forEach(idxName => store.deleteIndex(idxName)); + } + }); + const contentUpgrade = version._cfg.contentUpgrade; + if (contentUpgrade && version._cfg.version > oldVersion) { + generateMiddlewareStacks(db, idbUpgradeTrans); + trans._memoizedTables = {}; + let upgradeSchema = shallowClone(newSchema); + diff.del.forEach(table => { + upgradeSchema[table] = oldSchema[table]; + }); + removeTablesApi(db, [db.Transaction.prototype]); + setApiOnPlace(db, [db.Transaction.prototype], keys(upgradeSchema), upgradeSchema); + trans.schema = upgradeSchema; + const contentUpgradeIsAsync = isAsyncFunction(contentUpgrade); + if (contentUpgradeIsAsync) { + incrementExpectedAwaits(); + } + let returnValue; + const promiseFollowed = DexiePromise.follow(() => { + returnValue = contentUpgrade(trans); + if (returnValue) { + if (contentUpgradeIsAsync) { + var decrementor = decrementExpectedAwaits.bind(null, null); + returnValue.then(decrementor, decrementor); + } + } + }); + return (returnValue && typeof returnValue.then === 'function' ? + DexiePromise.resolve(returnValue) : promiseFollowed.then(() => returnValue)); + } + }); + queue.push(idbtrans => { + const newSchema = version._cfg.dbschema; + deleteRemovedTables(newSchema, idbtrans); + removeTablesApi(db, [db.Transaction.prototype]); + setApiOnPlace(db, [db.Transaction.prototype], db._storeNames, db._dbSchema); + trans.schema = db._dbSchema; + }); + queue.push(idbtrans => { + if (db.idbdb.objectStoreNames.contains('$meta')) { + if (Math.ceil(db.idbdb.version / 10) === version._cfg.version) { + db.idbdb.deleteObjectStore('$meta'); + delete db._dbSchema.$meta; + db._storeNames = db._storeNames.filter(name => name !== '$meta'); + } + else { + idbtrans.objectStore('$meta').put(version._cfg.version, 'version'); + } + } + }); + }); + function runQueue() { + return queue.length ? DexiePromise.resolve(queue.shift()(trans.idbtrans)).then(runQueue) : + DexiePromise.resolve(); + } + return runQueue().then(() => { + createMissingTables(globalSchema, idbUpgradeTrans); + }); +} +function getSchemaDiff(oldSchema, newSchema) { + const diff = { + del: [], + add: [], + change: [] + }; + let table; + for (table in oldSchema) { + if (!newSchema[table]) + diff.del.push(table); + } + for (table in newSchema) { + const oldDef = oldSchema[table], newDef = newSchema[table]; + if (!oldDef) { + diff.add.push([table, newDef]); + } + else { + const change = { + name: table, + def: newDef, + recreate: false, + del: [], + add: [], + change: [] + }; + if (( + '' + (oldDef.primKey.keyPath || '')) !== ('' + (newDef.primKey.keyPath || '')) || + (oldDef.primKey.auto !== newDef.primKey.auto)) { + change.recreate = true; + diff.change.push(change); + } + else { + const oldIndexes = oldDef.idxByName; + const newIndexes = newDef.idxByName; + let idxName; + for (idxName in oldIndexes) { + if (!newIndexes[idxName]) + change.del.push(idxName); + } + for (idxName in newIndexes) { + const oldIdx = oldIndexes[idxName], newIdx = newIndexes[idxName]; + if (!oldIdx) + change.add.push(newIdx); + else if (oldIdx.src !== newIdx.src) + change.change.push(newIdx); + } + if (change.del.length > 0 || change.add.length > 0 || change.change.length > 0) { + diff.change.push(change); + } + } + } + } + return diff; +} +function createTable(idbtrans, tableName, primKey, indexes) { + const store = idbtrans.db.createObjectStore(tableName, primKey.keyPath ? + { keyPath: primKey.keyPath, autoIncrement: primKey.auto } : + { autoIncrement: primKey.auto }); + indexes.forEach(idx => addIndex(store, idx)); + return store; +} +function createMissingTables(newSchema, idbtrans) { + keys(newSchema).forEach(tableName => { + if (!idbtrans.db.objectStoreNames.contains(tableName)) { + if (debug) + console.debug('Dexie: Creating missing table', tableName); + createTable(idbtrans, tableName, newSchema[tableName].primKey, newSchema[tableName].indexes); + } + }); +} +function deleteRemovedTables(newSchema, idbtrans) { + [].slice.call(idbtrans.db.objectStoreNames).forEach(storeName => newSchema[storeName] == null && idbtrans.db.deleteObjectStore(storeName)); +} +function addIndex(store, idx) { + store.createIndex(idx.name, idx.keyPath, { unique: idx.unique, multiEntry: idx.multi }); +} +function buildGlobalSchema(db, idbdb, tmpTrans) { + const globalSchema = {}; + const dbStoreNames = slice(idbdb.objectStoreNames, 0); + dbStoreNames.forEach(storeName => { + const store = tmpTrans.objectStore(storeName); + let keyPath = store.keyPath; + const primKey = createIndexSpec(nameFromKeyPath(keyPath), keyPath || "", true, false, !!store.autoIncrement, keyPath && typeof keyPath !== "string", true); + const indexes = []; + for (let j = 0; j < store.indexNames.length; ++j) { + const idbindex = store.index(store.indexNames[j]); + keyPath = idbindex.keyPath; + var index = createIndexSpec(idbindex.name, keyPath, !!idbindex.unique, !!idbindex.multiEntry, false, keyPath && typeof keyPath !== "string", false); + indexes.push(index); + } + globalSchema[storeName] = createTableSchema(storeName, primKey, indexes); + }); + return globalSchema; +} +function readGlobalSchema(db, idbdb, tmpTrans) { + db.verno = idbdb.version / 10; + const globalSchema = db._dbSchema = buildGlobalSchema(db, idbdb, tmpTrans); + db._storeNames = slice(idbdb.objectStoreNames, 0); + setApiOnPlace(db, [db._allTables], keys(globalSchema), globalSchema); +} +function verifyInstalledSchema(db, tmpTrans) { + const installedSchema = buildGlobalSchema(db, db.idbdb, tmpTrans); + const diff = getSchemaDiff(installedSchema, db._dbSchema); + return !(diff.add.length || diff.change.some(ch => ch.add.length || ch.change.length)); +} +function adjustToExistingIndexNames(db, schema, idbtrans) { + const storeNames = idbtrans.db.objectStoreNames; + for (let i = 0; i < storeNames.length; ++i) { + const storeName = storeNames[i]; + const store = idbtrans.objectStore(storeName); + db._hasGetAll = 'getAll' in store; + for (let j = 0; j < store.indexNames.length; ++j) { + const indexName = store.indexNames[j]; + const keyPath = store.index(indexName).keyPath; + const dexieName = typeof keyPath === 'string' ? keyPath : "[" + slice(keyPath).join('+') + "]"; + if (schema[storeName]) { + const indexSpec = schema[storeName].idxByName[dexieName]; + if (indexSpec) { + indexSpec.name = indexName; + delete schema[storeName].idxByName[dexieName]; + schema[storeName].idxByName[indexName] = indexSpec; + } + } + } + } + if (typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) && + !/(Chrome\/|Edge\/)/.test(navigator.userAgent) && + _global.WorkerGlobalScope && _global instanceof _global.WorkerGlobalScope && + [].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1] < 604) { + db._hasGetAll = false; + } +} +function parseIndexSyntax(primKeyAndIndexes) { + return primKeyAndIndexes.split(',').map((index, indexNum) => { + const typeSplit = index.split(':'); + const type = typeSplit[1]?.trim(); + index = typeSplit[0].trim(); + const name = index.replace(/([&*]|\+\+)/g, ""); + const keyPath = /^\[/.test(name) ? name.match(/^\[(.*)\]$/)[1].split('+') : name; + return createIndexSpec(name, keyPath || null, /\&/.test(index), /\*/.test(index), /\+\+/.test(index), isArray(keyPath), indexNum === 0, type); + }); +} + +class Version { + _createTableSchema(name, primKey, indexes) { + return createTableSchema(name, primKey, indexes); + } + _parseIndexSyntax(primKeyAndIndexes) { + return parseIndexSyntax(primKeyAndIndexes); + } + _parseStoresSpec(stores, outSchema) { + keys(stores).forEach((tableName) => { + if (stores[tableName] !== null) { + let indexes = this._parseIndexSyntax(stores[tableName]); + const primKey = indexes.shift(); + if (!primKey) { + throw new exceptions.Schema('Invalid schema for table ' + tableName + ': ' + stores[tableName]); + } + primKey.unique = true; + if (primKey.multi) + throw new exceptions.Schema('Primary key cannot be multiEntry*'); + indexes.forEach((idx) => { + if (idx.auto) + throw new exceptions.Schema('Only primary key can be marked as autoIncrement (++)'); + if (!idx.keyPath) + throw new exceptions.Schema('Index must have a name and cannot be an empty string'); + }); + const tblSchema = this._createTableSchema(tableName, primKey, indexes); + outSchema[tableName] = tblSchema; + } + }); + } + stores(stores) { + const db = this.db; + this._cfg.storesSource = this._cfg.storesSource + ? extend(this._cfg.storesSource, stores) + : stores; + const versions = db._versions; + const storesSpec = {}; + let dbschema = {}; + versions.forEach((version) => { + extend(storesSpec, version._cfg.storesSource); + dbschema = version._cfg.dbschema = {}; + version._parseStoresSpec(storesSpec, dbschema); + }); + db._dbSchema = dbschema; + removeTablesApi(db, [db._allTables, db, db.Transaction.prototype]); + setApiOnPlace(db, [db._allTables, db, db.Transaction.prototype, this._cfg.tables], keys(dbschema), dbschema); + db._storeNames = keys(dbschema); + return this; + } + upgrade(upgradeFunction) { + this._cfg.contentUpgrade = promisableChain(this._cfg.contentUpgrade || nop, upgradeFunction); + return this; + } +} + +function createVersionConstructor(db) { + return makeClassConstructor(Version.prototype, function Version(versionNumber) { + this.db = db; + this._cfg = { + version: versionNumber, + storesSource: null, + dbschema: {}, + tables: {}, + contentUpgrade: null + }; + }); +} + +function getDbNamesTable(indexedDB, IDBKeyRange) { + let dbNamesDB = indexedDB["_dbNamesDB"]; + if (!dbNamesDB) { + dbNamesDB = indexedDB["_dbNamesDB"] = new Dexie$1(DBNAMES_DB, { + addons: [], + indexedDB, + IDBKeyRange, + }); + dbNamesDB.version(1).stores({ dbnames: "name" }); + } + return dbNamesDB.table("dbnames"); +} +function hasDatabasesNative(indexedDB) { + return indexedDB && typeof indexedDB.databases === "function"; +} +function getDatabaseNames({ indexedDB, IDBKeyRange, }) { + return hasDatabasesNative(indexedDB) + ? Promise.resolve(indexedDB.databases()).then((infos) => infos + .map((info) => info.name) + .filter((name) => name !== DBNAMES_DB)) + : getDbNamesTable(indexedDB, IDBKeyRange).toCollection().primaryKeys(); +} +function _onDatabaseCreated({ indexedDB, IDBKeyRange }, name) { + !hasDatabasesNative(indexedDB) && + name !== DBNAMES_DB && + getDbNamesTable(indexedDB, IDBKeyRange).put({ name }).catch(nop); +} +function _onDatabaseDeleted({ indexedDB, IDBKeyRange }, name) { + !hasDatabasesNative(indexedDB) && + name !== DBNAMES_DB && + getDbNamesTable(indexedDB, IDBKeyRange).delete(name).catch(nop); +} + +function vip(fn) { + return newScope(function () { + PSD.letThrough = true; + return fn(); + }); +} + +function idbReady() { + var isSafari = !navigator.userAgentData && + /Safari\//.test(navigator.userAgent) && + !/Chrom(e|ium)\//.test(navigator.userAgent); + if (!isSafari || !indexedDB.databases) + return Promise.resolve(); + var intervalId; + return new Promise(function (resolve) { + var tryIdb = function () { return indexedDB.databases().finally(resolve); }; + intervalId = setInterval(tryIdb, 100); + tryIdb(); + }).finally(function () { return clearInterval(intervalId); }); +} + +function isEmptyRange(node) { + return !("from" in node); +} +const RangeSet = function (fromOrTree, to) { + if (this) { + extend(this, arguments.length ? { d: 1, from: fromOrTree, to: arguments.length > 1 ? to : fromOrTree } : { d: 0 }); + } + else { + const rv = new RangeSet(); + if (fromOrTree && ("d" in fromOrTree)) { + extend(rv, fromOrTree); + } + return rv; + } +}; +props(RangeSet.prototype, { + add(rangeSet) { + mergeRanges(this, rangeSet); + return this; + }, + addKey(key) { + addRange(this, key, key); + return this; + }, + addKeys(keys) { + keys.forEach(key => addRange(this, key, key)); + return this; + }, + hasKey(key) { + const node = getRangeSetIterator(this).next(key).value; + return node && cmp(node.from, key) <= 0 && cmp(node.to, key) >= 0; + }, + [iteratorSymbol]() { + return getRangeSetIterator(this); + } +}); +function addRange(target, from, to) { + const diff = cmp(from, to); + if (isNaN(diff)) + return; + if (diff > 0) + throw RangeError(); + if (isEmptyRange(target)) + return extend(target, { from, to, d: 1 }); + const left = target.l; + const right = target.r; + if (cmp(to, target.from) < 0) { + left + ? addRange(left, from, to) + : (target.l = { from, to, d: 1, l: null, r: null }); + return rebalance(target); + } + if (cmp(from, target.to) > 0) { + right + ? addRange(right, from, to) + : (target.r = { from, to, d: 1, l: null, r: null }); + return rebalance(target); + } + if (cmp(from, target.from) < 0) { + target.from = from; + target.l = null; + target.d = right ? right.d + 1 : 1; + } + if (cmp(to, target.to) > 0) { + target.to = to; + target.r = null; + target.d = target.l ? target.l.d + 1 : 1; + } + const rightWasCutOff = !target.r; + if (left && !target.l) { + mergeRanges(target, left); + } + if (right && rightWasCutOff) { + mergeRanges(target, right); + } +} +function mergeRanges(target, newSet) { + function _addRangeSet(target, { from, to, l, r }) { + addRange(target, from, to); + if (l) + _addRangeSet(target, l); + if (r) + _addRangeSet(target, r); + } + if (!isEmptyRange(newSet)) + _addRangeSet(target, newSet); +} +function rangesOverlap(rangeSet1, rangeSet2) { + const i1 = getRangeSetIterator(rangeSet2); + let nextResult1 = i1.next(); + if (nextResult1.done) + return false; + let a = nextResult1.value; + const i2 = getRangeSetIterator(rangeSet1); + let nextResult2 = i2.next(a.from); + let b = nextResult2.value; + while (!nextResult1.done && !nextResult2.done) { + if (cmp(b.from, a.to) <= 0 && cmp(b.to, a.from) >= 0) + return true; + cmp(a.from, b.from) < 0 + ? (a = (nextResult1 = i1.next(b.from)).value) + : (b = (nextResult2 = i2.next(a.from)).value); + } + return false; +} +function getRangeSetIterator(node) { + let state = isEmptyRange(node) ? null : { s: 0, n: node }; + return { + next(key) { + const keyProvided = arguments.length > 0; + while (state) { + switch (state.s) { + case 0: + state.s = 1; + if (keyProvided) { + while (state.n.l && cmp(key, state.n.from) < 0) + state = { up: state, n: state.n.l, s: 1 }; + } + else { + while (state.n.l) + state = { up: state, n: state.n.l, s: 1 }; + } + case 1: + state.s = 2; + if (!keyProvided || cmp(key, state.n.to) <= 0) + return { value: state.n, done: false }; + case 2: + if (state.n.r) { + state.s = 3; + state = { up: state, n: state.n.r, s: 0 }; + continue; + } + case 3: + state = state.up; + } + } + return { done: true }; + }, + }; +} +function rebalance(target) { + const diff = (target.r?.d || 0) - (target.l?.d || 0); + const r = diff > 1 ? "r" : diff < -1 ? "l" : ""; + if (r) { + const l = r === "r" ? "l" : "r"; + const rootClone = { ...target }; + const oldRootRight = target[r]; + target.from = oldRootRight.from; + target.to = oldRootRight.to; + target[r] = oldRootRight[r]; + rootClone[r] = oldRootRight[l]; + target[l] = rootClone; + rootClone.d = computeDepth(rootClone); + } + target.d = computeDepth(target); +} +function computeDepth({ r, l }) { + return (r ? (l ? Math.max(r.d, l.d) : r.d) : l ? l.d : 0) + 1; +} + +function extendObservabilitySet(target, newSet) { + keys(newSet).forEach(part => { + if (target[part]) + mergeRanges(target[part], newSet[part]); + else + target[part] = cloneSimpleObjectTree(newSet[part]); + }); + return target; +} + +function obsSetsOverlap(os1, os2) { + return os1.all || os2.all || Object.keys(os1).some((key) => os2[key] && rangesOverlap(os2[key], os1[key])); +} + +const cache = {}; + +let unsignaledParts = {}; +let isTaskEnqueued = false; +function signalSubscribersLazily(part, optimistic = false) { + extendObservabilitySet(unsignaledParts, part); + if (!isTaskEnqueued) { + isTaskEnqueued = true; + setTimeout(() => { + isTaskEnqueued = false; + const parts = unsignaledParts; + unsignaledParts = {}; + signalSubscribersNow(parts, false); + }, 0); + } +} +function signalSubscribersNow(updatedParts, deleteAffectedCacheEntries = false) { + const queriesToSignal = new Set(); + if (updatedParts.all) { + for (const tblCache of Object.values(cache)) { + collectTableSubscribers(tblCache, updatedParts, queriesToSignal, deleteAffectedCacheEntries); + } + } + else { + for (const key in updatedParts) { + const parts = /^idb\:\/\/(.*)\/(.*)\//.exec(key); + if (parts) { + const [, dbName, tableName] = parts; + const tblCache = cache[`idb://${dbName}/${tableName}`]; + if (tblCache) + collectTableSubscribers(tblCache, updatedParts, queriesToSignal, deleteAffectedCacheEntries); + } + } + } + queriesToSignal.forEach((requery) => requery()); +} +function collectTableSubscribers(tblCache, updatedParts, outQueriesToSignal, deleteAffectedCacheEntries) { + const updatedEntryLists = []; + for (const [indexName, entries] of Object.entries(tblCache.queries.query)) { + const filteredEntries = []; + for (const entry of entries) { + if (obsSetsOverlap(updatedParts, entry.obsSet)) { + entry.subscribers.forEach((requery) => outQueriesToSignal.add(requery)); + } + else if (deleteAffectedCacheEntries) { + filteredEntries.push(entry); + } + } + if (deleteAffectedCacheEntries) + updatedEntryLists.push([indexName, filteredEntries]); + } + if (deleteAffectedCacheEntries) { + for (const [indexName, filteredEntries] of updatedEntryLists) { + tblCache.queries.query[indexName] = filteredEntries; + } + } +} + +function dexieOpen(db) { + const state = db._state; + const { indexedDB } = db._deps; + if (state.isBeingOpened || db.idbdb) + return state.dbReadyPromise.then(() => state.dbOpenError ? + rejection(state.dbOpenError) : + db); + state.isBeingOpened = true; + state.dbOpenError = null; + state.openComplete = false; + const openCanceller = state.openCanceller; + let nativeVerToOpen = Math.round(db.verno * 10); + let schemaPatchMode = false; + function throwIfCancelled() { + if (state.openCanceller !== openCanceller) + throw new exceptions.DatabaseClosed('db.open() was cancelled'); + } + let resolveDbReady = state.dbReadyResolve, + upgradeTransaction = null, wasCreated = false; + const tryOpenDB = () => new DexiePromise((resolve, reject) => { + throwIfCancelled(); + if (!indexedDB) + throw new exceptions.MissingAPI(); + const dbName = db.name; + const req = state.autoSchema || !nativeVerToOpen ? + indexedDB.open(dbName) : + indexedDB.open(dbName, nativeVerToOpen); + if (!req) + throw new exceptions.MissingAPI(); + req.onerror = eventRejectHandler(reject); + req.onblocked = wrap(db._fireOnBlocked); + req.onupgradeneeded = wrap(e => { + upgradeTransaction = req.transaction; + if (state.autoSchema && !db._options.allowEmptyDB) { + req.onerror = preventDefault; + upgradeTransaction.abort(); + req.result.close(); + const delreq = indexedDB.deleteDatabase(dbName); + delreq.onsuccess = delreq.onerror = wrap(() => { + reject(new exceptions.NoSuchDatabase(`Database ${dbName} doesnt exist`)); + }); + } + else { + upgradeTransaction.onerror = eventRejectHandler(reject); + const oldVer = e.oldVersion > Math.pow(2, 62) ? 0 : e.oldVersion; + wasCreated = oldVer < 1; + db.idbdb = req.result; + if (schemaPatchMode) { + patchCurrentVersion(db, upgradeTransaction); + } + runUpgraders(db, oldVer / 10, upgradeTransaction, reject); + } + }, reject); + req.onsuccess = wrap(() => { + upgradeTransaction = null; + const idbdb = db.idbdb = req.result; + const objectStoreNames = slice(idbdb.objectStoreNames); + if (objectStoreNames.length > 0) + try { + const tmpTrans = idbdb.transaction(safariMultiStoreFix(objectStoreNames), 'readonly'); + if (state.autoSchema) + readGlobalSchema(db, idbdb, tmpTrans); + else { + adjustToExistingIndexNames(db, db._dbSchema, tmpTrans); + if (!verifyInstalledSchema(db, tmpTrans) && !schemaPatchMode) { + console.warn(`Dexie SchemaDiff: Schema was extended without increasing the number passed to db.version(). Dexie will add missing parts and increment native version number to workaround this.`); + idbdb.close(); + nativeVerToOpen = idbdb.version + 1; + schemaPatchMode = true; + return resolve(tryOpenDB()); + } + } + generateMiddlewareStacks(db, tmpTrans); + } + catch (e) { + } + connections.push(db); + idbdb.onversionchange = wrap(ev => { + state.vcFired = true; + db.on("versionchange").fire(ev); + }); + idbdb.onclose = wrap(() => { + db.close({ disableAutoOpen: false }); + }); + if (wasCreated) + _onDatabaseCreated(db._deps, dbName); + resolve(); + }, reject); + }).catch(err => { + switch (err?.name) { + case "UnknownError": + if (state.PR1398_maxLoop > 0) { + state.PR1398_maxLoop--; + console.warn('Dexie: Workaround for Chrome UnknownError on open()'); + return tryOpenDB(); + } + break; + case "VersionError": + if (nativeVerToOpen > 0) { + nativeVerToOpen = 0; + return tryOpenDB(); + } + break; + } + return DexiePromise.reject(err); + }); + return DexiePromise.race([ + openCanceller, + (typeof navigator === 'undefined' ? DexiePromise.resolve() : idbReady()).then(tryOpenDB) + ]).then(() => { + throwIfCancelled(); + state.onReadyBeingFired = []; + return DexiePromise.resolve(vip(() => db.on.ready.fire(db.vip))).then(function fireRemainders() { + if (state.onReadyBeingFired.length > 0) { + let remainders = state.onReadyBeingFired.reduce(promisableChain, nop); + state.onReadyBeingFired = []; + return DexiePromise.resolve(vip(() => remainders(db.vip))).then(fireRemainders); + } + }); + }).finally(() => { + if (state.openCanceller === openCanceller) { + state.onReadyBeingFired = null; + state.isBeingOpened = false; + } + }).catch(err => { + state.dbOpenError = err; + try { + upgradeTransaction && upgradeTransaction.abort(); + } + catch { } + if (openCanceller === state.openCanceller) { + db._close(); + } + return rejection(err); + }).finally(() => { + state.openComplete = true; + resolveDbReady(); + }).then(() => { + if (wasCreated) { + const everything = {}; + db.tables.forEach(table => { + table.schema.indexes.forEach(idx => { + if (idx.name) + everything[`idb://${db.name}/${table.name}/${idx.name}`] = new RangeSet(-Infinity, [[[]]]); + }); + everything[`idb://${db.name}/${table.name}/`] = everything[`idb://${db.name}/${table.name}/:dels`] = new RangeSet(-Infinity, [[[]]]); + }); + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME).fire(everything); + signalSubscribersNow(everything, true); + } + return db; + }); +} + +function awaitIterator(iterator) { + var callNext = result => iterator.next(result), doThrow = error => iterator.throw(error), onSuccess = step(callNext), onError = step(doThrow); + function step(getNext) { + return (val) => { + var next = getNext(val), value = next.value; + return next.done ? value : + (!value || typeof value.then !== 'function' ? + isArray(value) ? Promise.all(value).then(onSuccess, onError) : onSuccess(value) : + value.then(onSuccess, onError)); + }; + } + return step(callNext)(); +} + +function extractTransactionArgs(mode, _tableArgs_, scopeFunc) { + var i = arguments.length; + if (i < 2) + throw new exceptions.InvalidArgument("Too few arguments"); + var args = new Array(i - 1); + while (--i) + args[i - 1] = arguments[i]; + scopeFunc = args.pop(); + var tables = flatten(args); + return [mode, tables, scopeFunc]; +} +function enterTransactionScope(db, mode, storeNames, parentTransaction, scopeFunc) { + return DexiePromise.resolve().then(() => { + const transless = PSD.transless || PSD; + const trans = db._createTransaction(mode, storeNames, db._dbSchema, parentTransaction); + trans.explicit = true; + const zoneProps = { + trans: trans, + transless: transless + }; + if (parentTransaction) { + trans.idbtrans = parentTransaction.idbtrans; + } + else { + try { + trans.create(); + trans.idbtrans._explicit = true; + db._state.PR1398_maxLoop = 3; + } + catch (ex) { + if (ex.name === errnames.InvalidState && db.isOpen() && --db._state.PR1398_maxLoop > 0) { + console.warn('Dexie: Need to reopen db'); + db.close({ disableAutoOpen: false }); + return db.open().then(() => enterTransactionScope(db, mode, storeNames, null, scopeFunc)); + } + return rejection(ex); + } + } + const scopeFuncIsAsync = isAsyncFunction(scopeFunc); + if (scopeFuncIsAsync) { + incrementExpectedAwaits(); + } + let returnValue; + const promiseFollowed = DexiePromise.follow(() => { + returnValue = scopeFunc.call(trans, trans); + if (returnValue) { + if (scopeFuncIsAsync) { + var decrementor = decrementExpectedAwaits.bind(null, null); + returnValue.then(decrementor, decrementor); + } + else if (typeof returnValue.next === 'function' && typeof returnValue.throw === 'function') { + returnValue = awaitIterator(returnValue); + } + } + }, zoneProps); + return (returnValue && typeof returnValue.then === 'function' ? + DexiePromise.resolve(returnValue).then(x => trans.active ? + x + : rejection(new exceptions.PrematureCommit("Transaction committed too early. See http://bit.ly/2kdckMn"))) + : promiseFollowed.then(() => returnValue)).then(x => { + if (parentTransaction) + trans._resolve(); + return trans._completion.then(() => x); + }).catch(e => { + trans._reject(e); + return rejection(e); + }); + }); +} + +function pad(a, value, count) { + const result = isArray(a) ? a.slice() : [a]; + for (let i = 0; i < count; ++i) + result.push(value); + return result; +} +function createVirtualIndexMiddleware(down) { + return { + ...down, + table(tableName) { + const table = down.table(tableName); + const { schema } = table; + const indexLookup = {}; + const allVirtualIndexes = []; + function addVirtualIndexes(keyPath, keyTail, lowLevelIndex) { + const keyPathAlias = getKeyPathAlias(keyPath); + const indexList = (indexLookup[keyPathAlias] = indexLookup[keyPathAlias] || []); + const keyLength = keyPath == null ? 0 : typeof keyPath === 'string' ? 1 : keyPath.length; + const isVirtual = keyTail > 0; + const virtualIndex = { + ...lowLevelIndex, + name: isVirtual + ? `${keyPathAlias}(virtual-from:${lowLevelIndex.name})` + : lowLevelIndex.name, + lowLevelIndex, + isVirtual, + keyTail, + keyLength, + extractKey: getKeyExtractor(keyPath), + unique: !isVirtual && lowLevelIndex.unique + }; + indexList.push(virtualIndex); + if (!virtualIndex.isPrimaryKey) { + allVirtualIndexes.push(virtualIndex); + } + if (keyLength > 1) { + const virtualKeyPath = keyLength === 2 ? + keyPath[0] : + keyPath.slice(0, keyLength - 1); + addVirtualIndexes(virtualKeyPath, keyTail + 1, lowLevelIndex); + } + indexList.sort((a, b) => a.keyTail - b.keyTail); + return virtualIndex; + } + const primaryKey = addVirtualIndexes(schema.primaryKey.keyPath, 0, schema.primaryKey); + indexLookup[":id"] = [primaryKey]; + for (const index of schema.indexes) { + addVirtualIndexes(index.keyPath, 0, index); + } + function findBestIndex(keyPath) { + const result = indexLookup[getKeyPathAlias(keyPath)]; + return result && result[0]; + } + function translateRange(range, keyTail) { + return { + type: range.type === 1 ? + 2 : + range.type, + lower: pad(range.lower, range.lowerOpen ? down.MAX_KEY : down.MIN_KEY, keyTail), + lowerOpen: true, + upper: pad(range.upper, range.upperOpen ? down.MIN_KEY : down.MAX_KEY, keyTail), + upperOpen: true + }; + } + function translateRequest(req) { + const index = req.query.index; + return index.isVirtual ? { + ...req, + query: { + index: index.lowLevelIndex, + range: translateRange(req.query.range, index.keyTail) + } + } : req; + } + const result = { + ...table, + schema: { + ...schema, + primaryKey, + indexes: allVirtualIndexes, + getIndexByKeyPath: findBestIndex + }, + count(req) { + return table.count(translateRequest(req)); + }, + query(req) { + return table.query(translateRequest(req)); + }, + openCursor(req) { + const { keyTail, isVirtual, keyLength } = req.query.index; + if (!isVirtual) + return table.openCursor(req); + function createVirtualCursor(cursor) { + function _continue(key) { + key != null ? + cursor.continue(pad(key, req.reverse ? down.MAX_KEY : down.MIN_KEY, keyTail)) : + req.unique ? + cursor.continue(cursor.key.slice(0, keyLength) + .concat(req.reverse + ? down.MIN_KEY + : down.MAX_KEY, keyTail)) : + cursor.continue(); + } + const virtualCursor = Object.create(cursor, { + continue: { value: _continue }, + continuePrimaryKey: { + value(key, primaryKey) { + cursor.continuePrimaryKey(pad(key, down.MAX_KEY, keyTail), primaryKey); + } + }, + primaryKey: { + get() { + return cursor.primaryKey; + } + }, + key: { + get() { + const key = cursor.key; + return keyLength === 1 ? + key[0] : + key.slice(0, keyLength); + } + }, + value: { + get() { + return cursor.value; + } + } + }); + return virtualCursor; + } + return table.openCursor(translateRequest(req)) + .then(cursor => cursor && createVirtualCursor(cursor)); + } + }; + return result; + } + }; +} +const virtualIndexMiddleware = { + stack: "dbcore", + name: "VirtualIndexMiddleware", + level: 1, + create: createVirtualIndexMiddleware +}; + +function getObjectDiff(a, b, rv, prfx) { + rv = rv || {}; + prfx = prfx || ''; + keys(a).forEach((prop) => { + if (!hasOwn(b, prop)) { + rv[prfx + prop] = undefined; + } + else { + var ap = a[prop], bp = b[prop]; + if (typeof ap === 'object' && typeof bp === 'object' && ap && bp) { + const apTypeName = toStringTag(ap); + const bpTypeName = toStringTag(bp); + if (apTypeName !== bpTypeName) { + rv[prfx + prop] = b[prop]; + } + else if (apTypeName === 'Object') { + getObjectDiff(ap, bp, rv, prfx + prop + '.'); + } + else if (ap !== bp) { + rv[prfx + prop] = b[prop]; + } + } + else if (ap !== bp) + rv[prfx + prop] = b[prop]; + } + }); + keys(b).forEach((prop) => { + if (!hasOwn(a, prop)) { + rv[prfx + prop] = b[prop]; + } + }); + return rv; +} + +function getEffectiveKeys(primaryKey, req) { + if (req.type === 'delete') + return req.keys; + return req.keys || req.values.map(primaryKey.extractKey); +} + +const hooksMiddleware = { + stack: "dbcore", + name: "HooksMiddleware", + level: 2, + create: (downCore) => ({ + ...downCore, + table(tableName) { + const downTable = downCore.table(tableName); + const { primaryKey } = downTable.schema; + const tableMiddleware = { + ...downTable, + mutate(req) { + const dxTrans = PSD.trans; + const { deleting, creating, updating } = dxTrans.table(tableName).hook; + switch (req.type) { + case 'add': + if (creating.fire === nop) + break; + return dxTrans._promise('readwrite', () => addPutOrDelete(req), true); + case 'put': + if (creating.fire === nop && updating.fire === nop) + break; + return dxTrans._promise('readwrite', () => addPutOrDelete(req), true); + case 'delete': + if (deleting.fire === nop) + break; + return dxTrans._promise('readwrite', () => addPutOrDelete(req), true); + case 'deleteRange': + if (deleting.fire === nop) + break; + return dxTrans._promise('readwrite', () => deleteRange(req), true); + } + return downTable.mutate(req); + function addPutOrDelete(req) { + const dxTrans = PSD.trans; + const keys = req.keys || getEffectiveKeys(primaryKey, req); + if (!keys) + throw new Error("Keys missing"); + req = req.type === 'add' || req.type === 'put' ? + { ...req, keys } : + { ...req }; + if (req.type !== 'delete') + req.values = [...req.values]; + if (req.keys) + req.keys = [...req.keys]; + return getExistingValues(downTable, req, keys).then(existingValues => { + const contexts = keys.map((key, i) => { + const existingValue = existingValues[i]; + const ctx = { onerror: null, onsuccess: null }; + if (req.type === 'delete') { + deleting.fire.call(ctx, key, existingValue, dxTrans); + } + else if (req.type === 'add' || existingValue === undefined) { + const generatedPrimaryKey = creating.fire.call(ctx, key, req.values[i], dxTrans); + if (key == null && generatedPrimaryKey != null) { + key = generatedPrimaryKey; + req.keys[i] = key; + if (!primaryKey.outbound) { + setByKeyPath(req.values[i], primaryKey.keyPath, key); + } + } + } + else { + const objectDiff = getObjectDiff(existingValue, req.values[i]); + const additionalChanges = updating.fire.call(ctx, objectDiff, key, existingValue, dxTrans); + if (additionalChanges) { + const requestedValue = req.values[i]; + Object.keys(additionalChanges).forEach(keyPath => { + if (hasOwn(requestedValue, keyPath)) { + requestedValue[keyPath] = additionalChanges[keyPath]; + } + else { + setByKeyPath(requestedValue, keyPath, additionalChanges[keyPath]); + } + }); + } + } + return ctx; + }); + return downTable.mutate(req).then(({ failures, results, numFailures, lastResult }) => { + for (let i = 0; i < keys.length; ++i) { + const primKey = results ? results[i] : keys[i]; + const ctx = contexts[i]; + if (primKey == null) { + ctx.onerror && ctx.onerror(failures[i]); + } + else { + ctx.onsuccess && ctx.onsuccess(req.type === 'put' && existingValues[i] ? + req.values[i] : + primKey + ); + } + } + return { failures, results, numFailures, lastResult }; + }).catch(error => { + contexts.forEach(ctx => ctx.onerror && ctx.onerror(error)); + return Promise.reject(error); + }); + }); + } + function deleteRange(req) { + return deleteNextChunk(req.trans, req.range, 10000); + } + function deleteNextChunk(trans, range, limit) { + return downTable.query({ trans, values: false, query: { index: primaryKey, range }, limit }) + .then(({ result }) => { + return addPutOrDelete({ type: 'delete', keys: result, trans }).then(res => { + if (res.numFailures > 0) + return Promise.reject(res.failures[0]); + if (result.length < limit) { + return { failures: [], numFailures: 0, lastResult: undefined }; + } + else { + return deleteNextChunk(trans, { ...range, lower: result[result.length - 1], lowerOpen: true }, limit); + } + }); + }); + } + } + }; + return tableMiddleware; + }, + }) +}; +function getExistingValues(table, req, effectiveKeys) { + return req.type === "add" + ? Promise.resolve([]) + : table.getMany({ trans: req.trans, keys: effectiveKeys, cache: "immutable" }); +} + +function getFromTransactionCache(keys, cache, clone) { + try { + if (!cache) + return null; + if (cache.keys.length < keys.length) + return null; + const result = []; + for (let i = 0, j = 0; i < cache.keys.length && j < keys.length; ++i) { + if (cmp(cache.keys[i], keys[j]) !== 0) + continue; + result.push(clone ? deepClone(cache.values[i]) : cache.values[i]); + ++j; + } + return result.length === keys.length ? result : null; + } + catch { + return null; + } +} +const cacheExistingValuesMiddleware = { + stack: "dbcore", + level: -1, + create: (core) => { + return { + table: (tableName) => { + const table = core.table(tableName); + return { + ...table, + getMany: (req) => { + if (!req.cache) { + return table.getMany(req); + } + const cachedResult = getFromTransactionCache(req.keys, req.trans["_cache"], req.cache === "clone"); + if (cachedResult) { + return DexiePromise.resolve(cachedResult); + } + return table.getMany(req).then((res) => { + req.trans["_cache"] = { + keys: req.keys, + values: req.cache === "clone" ? deepClone(res) : res, + }; + return res; + }); + }, + mutate: (req) => { + if (req.type !== "add") + req.trans["_cache"] = null; + return table.mutate(req); + }, + }; + }, + }; + }, +}; + +function isCachableContext(ctx, table) { + return (ctx.trans.mode === 'readonly' && + !!ctx.subscr && + !ctx.trans.explicit && + ctx.trans.db._options.cache !== 'disabled' && + !table.schema.primaryKey.outbound); +} + +function isCachableRequest(type, req) { + switch (type) { + case 'query': + return req.values && !req.unique; + case 'get': + return false; + case 'getMany': + return false; + case 'count': + return false; + case 'openCursor': + return false; + } +} + +const observabilityMiddleware = { + stack: "dbcore", + level: 0, + name: "Observability", + create: (core) => { + const dbName = core.schema.name; + const FULL_RANGE = new RangeSet(core.MIN_KEY, core.MAX_KEY); + return { + ...core, + transaction: (stores, mode, options) => { + if (PSD.subscr && mode !== 'readonly') { + throw new exceptions.ReadOnly(`Readwrite transaction in liveQuery context. Querier source: ${PSD.querier}`); + } + return core.transaction(stores, mode, options); + }, + table: (tableName) => { + const table = core.table(tableName); + const { schema } = table; + const { primaryKey, indexes } = schema; + const { extractKey, outbound } = primaryKey; + const indexesWithAutoIncPK = primaryKey.autoIncrement && indexes.filter((index) => index.compound && index.keyPath.includes(primaryKey.keyPath)); + const tableClone = { + ...table, + mutate: (req) => { + const trans = req.trans; + const mutatedParts = req.mutatedParts || (req.mutatedParts = {}); + const getRangeSet = (indexName) => { + const part = `idb://${dbName}/${tableName}/${indexName}`; + return (mutatedParts[part] || + (mutatedParts[part] = new RangeSet())); + }; + const pkRangeSet = getRangeSet(""); + const delsRangeSet = getRangeSet(":dels"); + const { type } = req; + let [keys, newObjs] = req.type === "deleteRange" + ? [req.range] + : req.type === "delete" + ? [req.keys] + : req.values.length < 50 + ? [getEffectiveKeys(primaryKey, req).filter(id => id), req.values] + : []; + const oldCache = req.trans["_cache"]; + if (isArray(keys)) { + pkRangeSet.addKeys(keys); + const oldObjs = type === 'delete' || keys.length === newObjs.length ? getFromTransactionCache(keys, oldCache) : null; + if (!oldObjs) { + delsRangeSet.addKeys(keys); + } + if (oldObjs || newObjs) { + trackAffectedIndexes(getRangeSet, schema, oldObjs, newObjs); + } + } + else if (keys) { + const range = { + from: keys.lower ?? core.MIN_KEY, + to: keys.upper ?? core.MAX_KEY + }; + delsRangeSet.add(range); + pkRangeSet.add(range); + } + else { + pkRangeSet.add(FULL_RANGE); + delsRangeSet.add(FULL_RANGE); + schema.indexes.forEach(idx => getRangeSet(idx.name).add(FULL_RANGE)); + } + return table.mutate(req).then((res) => { + if (keys && (req.type === 'add' || req.type === 'put')) { + pkRangeSet.addKeys(res.results); + if (indexesWithAutoIncPK) { + indexesWithAutoIncPK.forEach(idx => { + const idxVals = req.values.map(v => idx.extractKey(v)); + const pkPos = idx.keyPath.findIndex(prop => prop === primaryKey.keyPath); + for (let i = 0, len = res.results.length; i < len; ++i) { + idxVals[i][pkPos] = res.results[i]; + } + getRangeSet(idx.name).addKeys(idxVals); + }); + } + } + trans.mutatedParts = extendObservabilitySet(trans.mutatedParts || {}, mutatedParts); + return res; + }); + }, + }; + const getRange = ({ query: { index, range }, }) => [ + index, + new RangeSet(range.lower ?? core.MIN_KEY, range.upper ?? core.MAX_KEY), + ]; + const readSubscribers = { + get: (req) => [primaryKey, new RangeSet(req.key)], + getMany: (req) => [primaryKey, new RangeSet().addKeys(req.keys)], + count: getRange, + query: getRange, + openCursor: getRange, + }; + keys(readSubscribers).forEach((method) => { + tableClone[method] = function (req) { + const { subscr } = PSD; + const isLiveQuery = !!subscr; + let cachable = isCachableContext(PSD, table) && isCachableRequest(method, req); + const obsSet = cachable + ? req.obsSet = {} + : subscr; + if (isLiveQuery) { + const getRangeSet = (indexName) => { + const part = `idb://${dbName}/${tableName}/${indexName}`; + return (obsSet[part] || + (obsSet[part] = new RangeSet())); + }; + const pkRangeSet = getRangeSet(""); + const delsRangeSet = getRangeSet(":dels"); + const [queriedIndex, queriedRanges] = readSubscribers[method](req); + if (method === 'query' && queriedIndex.isPrimaryKey && !req.values) { + delsRangeSet.add(queriedRanges); + } + else { + getRangeSet(queriedIndex.name || "").add(queriedRanges); + } + if (!queriedIndex.isPrimaryKey) { + if (method === "count") { + delsRangeSet.add(FULL_RANGE); + } + else { + const keysPromise = method === "query" && + outbound && + req.values && + table.query({ + ...req, + values: false, + }); + return table[method].apply(this, arguments).then((res) => { + if (method === "query") { + if (outbound && req.values) { + return keysPromise.then(({ result: resultingKeys }) => { + pkRangeSet.addKeys(resultingKeys); + return res; + }); + } + const pKeys = req.values + ? res.result.map(extractKey) + : res.result; + if (req.values) { + pkRangeSet.addKeys(pKeys); + } + else { + delsRangeSet.addKeys(pKeys); + } + } + else if (method === "openCursor") { + const cursor = res; + const wantValues = req.values; + return (cursor && + Object.create(cursor, { + key: { + get() { + delsRangeSet.addKey(cursor.primaryKey); + return cursor.key; + }, + }, + primaryKey: { + get() { + const pkey = cursor.primaryKey; + delsRangeSet.addKey(pkey); + return pkey; + }, + }, + value: { + get() { + wantValues && pkRangeSet.addKey(cursor.primaryKey); + return cursor.value; + }, + }, + })); + } + return res; + }); + } + } + } + return table[method].apply(this, arguments); + }; + }); + return tableClone; + }, + }; + }, +}; +function trackAffectedIndexes(getRangeSet, schema, oldObjs, newObjs) { + function addAffectedIndex(ix) { + const rangeSet = getRangeSet(ix.name || ""); + function extractKey(obj) { + return obj != null ? ix.extractKey(obj) : null; + } + const addKeyOrKeys = (key) => ix.multiEntry && isArray(key) + ? key.forEach(key => rangeSet.addKey(key)) + : rangeSet.addKey(key); + (oldObjs || newObjs).forEach((_, i) => { + const oldKey = oldObjs && extractKey(oldObjs[i]); + const newKey = newObjs && extractKey(newObjs[i]); + if (cmp(oldKey, newKey) !== 0) { + if (oldKey != null) + addKeyOrKeys(oldKey); + if (newKey != null) + addKeyOrKeys(newKey); + } + }); + } + schema.indexes.forEach(addAffectedIndex); +} + +function adjustOptimisticFromFailures(tblCache, req, res) { + if (res.numFailures === 0) + return req; + if (req.type === 'deleteRange') { + return null; + } + const numBulkOps = req.keys + ? req.keys.length + : 'values' in req && req.values + ? req.values.length + : 1; + if (res.numFailures === numBulkOps) { + return null; + } + const clone = { ...req }; + if (isArray(clone.keys)) { + clone.keys = clone.keys.filter((_, i) => !(i in res.failures)); + } + if ('values' in clone && isArray(clone.values)) { + clone.values = clone.values.filter((_, i) => !(i in res.failures)); + } + return clone; +} + +function isAboveLower(key, range) { + return range.lower === undefined + ? true + : range.lowerOpen + ? cmp(key, range.lower) > 0 + : cmp(key, range.lower) >= 0; +} +function isBelowUpper(key, range) { + return range.upper === undefined + ? true + : range.upperOpen + ? cmp(key, range.upper) < 0 + : cmp(key, range.upper) <= 0; +} +function isWithinRange(key, range) { + return isAboveLower(key, range) && isBelowUpper(key, range); +} + +function applyOptimisticOps(result, req, ops, table, cacheEntry, immutable) { + if (!ops || ops.length === 0) + return result; + const index = req.query.index; + const { multiEntry } = index; + const queryRange = req.query.range; + const primaryKey = table.schema.primaryKey; + const extractPrimKey = primaryKey.extractKey; + const extractIndex = index.extractKey; + const extractLowLevelIndex = (index.lowLevelIndex || index).extractKey; + let finalResult = ops.reduce((result, op) => { + let modifedResult = result; + const includedValues = []; + if (op.type === 'add' || op.type === 'put') { + const includedPKs = new RangeSet(); + for (let i = op.values.length - 1; i >= 0; --i) { + const value = op.values[i]; + const pk = extractPrimKey(value); + if (includedPKs.hasKey(pk)) + continue; + const key = extractIndex(value); + if (multiEntry && isArray(key) + ? key.some((k) => isWithinRange(k, queryRange)) + : isWithinRange(key, queryRange)) { + includedPKs.addKey(pk); + includedValues.push(value); + } + } + } + switch (op.type) { + case 'add': { + const existingKeys = new RangeSet().addKeys(req.values ? result.map((v) => extractPrimKey(v)) : result); + modifedResult = result.concat(req.values + ? includedValues.filter((v) => { + const key = extractPrimKey(v); + if (existingKeys.hasKey(key)) + return false; + existingKeys.addKey(key); + return true; + }) + : includedValues + .map((v) => extractPrimKey(v)) + .filter((k) => { + if (existingKeys.hasKey(k)) + return false; + existingKeys.addKey(k); + return true; + })); + break; + } + case 'put': { + const keySet = new RangeSet().addKeys(op.values.map((v) => extractPrimKey(v))); + modifedResult = result + .filter( + (item) => !keySet.hasKey(req.values ? extractPrimKey(item) : item)) + .concat( + req.values + ? includedValues + : includedValues.map((v) => extractPrimKey(v))); + break; + } + case 'delete': + const keysToDelete = new RangeSet().addKeys(op.keys); + modifedResult = result.filter((item) => !keysToDelete.hasKey(req.values ? extractPrimKey(item) : item)); + break; + case 'deleteRange': + const range = op.range; + modifedResult = result.filter((item) => !isWithinRange(extractPrimKey(item), range)); + break; + } + return modifedResult; + }, result); + if (finalResult === result) + return result; + finalResult.sort((a, b) => cmp(extractLowLevelIndex(a), extractLowLevelIndex(b)) || + cmp(extractPrimKey(a), extractPrimKey(b))); + if (req.limit && req.limit < Infinity) { + if (finalResult.length > req.limit) { + finalResult.length = req.limit; + } + else if (result.length === req.limit && finalResult.length < req.limit) { + cacheEntry.dirty = true; + } + } + return immutable ? Object.freeze(finalResult) : finalResult; +} + +function areRangesEqual(r1, r2) { + return (cmp(r1.lower, r2.lower) === 0 && + cmp(r1.upper, r2.upper) === 0 && + !!r1.lowerOpen === !!r2.lowerOpen && + !!r1.upperOpen === !!r2.upperOpen); +} + +function compareLowers(lower1, lower2, lowerOpen1, lowerOpen2) { + if (lower1 === undefined) + return lower2 !== undefined ? -1 : 0; + if (lower2 === undefined) + return 1; + const c = cmp(lower1, lower2); + if (c === 0) { + if (lowerOpen1 && lowerOpen2) + return 0; + if (lowerOpen1) + return 1; + if (lowerOpen2) + return -1; + } + return c; +} +function compareUppers(upper1, upper2, upperOpen1, upperOpen2) { + if (upper1 === undefined) + return upper2 !== undefined ? 1 : 0; + if (upper2 === undefined) + return -1; + const c = cmp(upper1, upper2); + if (c === 0) { + if (upperOpen1 && upperOpen2) + return 0; + if (upperOpen1) + return -1; + if (upperOpen2) + return 1; + } + return c; +} +function isSuperRange(r1, r2) { + return (compareLowers(r1.lower, r2.lower, r1.lowerOpen, r2.lowerOpen) <= 0 && + compareUppers(r1.upper, r2.upper, r1.upperOpen, r2.upperOpen) >= 0); +} + +function findCompatibleQuery(dbName, tableName, type, req) { + const tblCache = cache[`idb://${dbName}/${tableName}`]; + if (!tblCache) + return []; + const queries = tblCache.queries[type]; + if (!queries) + return [null, false, tblCache, null]; + const indexName = req.query ? req.query.index.name : null; + const entries = queries[indexName || '']; + if (!entries) + return [null, false, tblCache, null]; + switch (type) { + case 'query': + const equalEntry = entries.find((entry) => entry.req.limit === req.limit && + entry.req.values === req.values && + areRangesEqual(entry.req.query.range, req.query.range)); + if (equalEntry) + return [ + equalEntry, + true, + tblCache, + entries, + ]; + const superEntry = entries.find((entry) => { + const limit = 'limit' in entry.req ? entry.req.limit : Infinity; + return (limit >= req.limit && + (req.values ? entry.req.values : true) && + isSuperRange(entry.req.query.range, req.query.range)); + }); + return [superEntry, false, tblCache, entries]; + case 'count': + const countQuery = entries.find((entry) => areRangesEqual(entry.req.query.range, req.query.range)); + return [countQuery, !!countQuery, tblCache, entries]; + } +} + +function subscribeToCacheEntry(cacheEntry, container, requery, signal) { + cacheEntry.subscribers.add(requery); + signal.addEventListener("abort", () => { + cacheEntry.subscribers.delete(requery); + if (cacheEntry.subscribers.size === 0) { + enqueForDeletion(cacheEntry, container); + } + }); +} +function enqueForDeletion(cacheEntry, container) { + setTimeout(() => { + if (cacheEntry.subscribers.size === 0) { + delArrayItem(container, cacheEntry); + } + }, 3000); +} + +const cacheMiddleware = { + stack: 'dbcore', + level: 0, + name: 'Cache', + create: (core) => { + const dbName = core.schema.name; + const coreMW = { + ...core, + transaction: (stores, mode, options) => { + const idbtrans = core.transaction(stores, mode, options); + if (mode === 'readwrite') { + const ac = new AbortController(); + const { signal } = ac; + const endTransaction = (wasCommitted) => () => { + ac.abort(); + if (mode === 'readwrite') { + const affectedSubscribers = new Set(); + for (const storeName of stores) { + const tblCache = cache[`idb://${dbName}/${storeName}`]; + if (tblCache) { + const table = core.table(storeName); + const ops = tblCache.optimisticOps.filter((op) => op.trans === idbtrans); + if (idbtrans._explicit && wasCommitted && idbtrans.mutatedParts) { + for (const entries of Object.values(tblCache.queries.query)) { + for (const entry of entries.slice()) { + if (obsSetsOverlap(entry.obsSet, idbtrans.mutatedParts)) { + delArrayItem(entries, entry); + entry.subscribers.forEach((requery) => affectedSubscribers.add(requery)); + } + } + } + } + else if (ops.length > 0) { + tblCache.optimisticOps = tblCache.optimisticOps.filter((op) => op.trans !== idbtrans); + for (const entries of Object.values(tblCache.queries.query)) { + for (const entry of entries.slice()) { + if (entry.res != null && + idbtrans.mutatedParts +) { + if (wasCommitted && !entry.dirty) { + const freezeResults = Object.isFrozen(entry.res); + const modRes = applyOptimisticOps(entry.res, entry.req, ops, table, entry, freezeResults); + if (entry.dirty) { + delArrayItem(entries, entry); + entry.subscribers.forEach((requery) => affectedSubscribers.add(requery)); + } + else if (modRes !== entry.res) { + entry.res = modRes; + entry.promise = DexiePromise.resolve({ result: modRes }); + } + } + else { + if (entry.dirty) { + delArrayItem(entries, entry); + } + entry.subscribers.forEach((requery) => affectedSubscribers.add(requery)); + } + } + } + } + } + } + } + affectedSubscribers.forEach((requery) => requery()); + } + }; + idbtrans.addEventListener('abort', endTransaction(false), { + signal, + }); + idbtrans.addEventListener('error', endTransaction(false), { + signal, + }); + idbtrans.addEventListener('complete', endTransaction(true), { + signal, + }); + } + return idbtrans; + }, + table(tableName) { + const downTable = core.table(tableName); + const primKey = downTable.schema.primaryKey; + const tableMW = { + ...downTable, + mutate(req) { + const trans = PSD.trans; + if (primKey.outbound || + trans.db._options.cache === 'disabled' || + trans.explicit || + trans.idbtrans.mode !== 'readwrite' + ) { + return downTable.mutate(req); + } + const tblCache = cache[`idb://${dbName}/${tableName}`]; + if (!tblCache) + return downTable.mutate(req); + const promise = downTable.mutate(req); + if ((req.type === 'add' || req.type === 'put') && (req.values.length >= 50 || getEffectiveKeys(primKey, req).some(key => key == null))) { + promise.then((res) => { + const reqWithResolvedKeys = { + ...req, + values: req.values.map((value, i) => { + if (res.failures[i]) + return value; + const valueWithKey = primKey.keyPath?.includes('.') + ? deepClone(value) + : { + ...value, + }; + setByKeyPath(valueWithKey, primKey.keyPath, res.results[i]); + return valueWithKey; + }) + }; + const adjustedReq = adjustOptimisticFromFailures(tblCache, reqWithResolvedKeys, res); + tblCache.optimisticOps.push(adjustedReq); + queueMicrotask(() => req.mutatedParts && signalSubscribersLazily(req.mutatedParts)); + }); + } + else { + tblCache.optimisticOps.push(req); + req.mutatedParts && signalSubscribersLazily(req.mutatedParts); + promise.then((res) => { + if (res.numFailures > 0) { + delArrayItem(tblCache.optimisticOps, req); + const adjustedReq = adjustOptimisticFromFailures(tblCache, req, res); + if (adjustedReq) { + tblCache.optimisticOps.push(adjustedReq); + } + req.mutatedParts && signalSubscribersLazily(req.mutatedParts); + } + }); + promise.catch(() => { + delArrayItem(tblCache.optimisticOps, req); + req.mutatedParts && signalSubscribersLazily(req.mutatedParts); + }); + } + return promise; + }, + query(req) { + if (!isCachableContext(PSD, downTable) || !isCachableRequest("query", req)) + return downTable.query(req); + const freezeResults = PSD.trans?.db._options.cache === 'immutable'; + const { requery, signal } = PSD; + let [cacheEntry, exactMatch, tblCache, container] = findCompatibleQuery(dbName, tableName, 'query', req); + if (cacheEntry && exactMatch) { + cacheEntry.obsSet = req.obsSet; + } + else { + const promise = downTable.query(req).then((res) => { + const result = res.result; + if (cacheEntry) + cacheEntry.res = result; + if (freezeResults) { + for (let i = 0, l = result.length; i < l; ++i) { + Object.freeze(result[i]); + } + Object.freeze(result); + } + else { + res.result = deepClone(result); + } + return res; + }).catch(error => { + if (container && cacheEntry) + delArrayItem(container, cacheEntry); + return Promise.reject(error); + }); + cacheEntry = { + obsSet: req.obsSet, + promise, + subscribers: new Set(), + type: 'query', + req, + dirty: false, + }; + if (container) { + container.push(cacheEntry); + } + else { + container = [cacheEntry]; + if (!tblCache) { + tblCache = cache[`idb://${dbName}/${tableName}`] = { + queries: { + query: {}, + count: {}, + }, + objs: new Map(), + optimisticOps: [], + unsignaledParts: {} + }; + } + tblCache.queries.query[req.query.index.name || ''] = container; + } + } + subscribeToCacheEntry(cacheEntry, container, requery, signal); + return cacheEntry.promise.then((res) => { + return { + result: applyOptimisticOps(res.result, req, tblCache?.optimisticOps, downTable, cacheEntry, freezeResults), + }; + }); + }, + }; + return tableMW; + }, + }; + return coreMW; + }, +}; + +function vipify(target, vipDb) { + return new Proxy(target, { + get(target, prop, receiver) { + if (prop === 'db') + return vipDb; + return Reflect.get(target, prop, receiver); + } + }); +} + +class Dexie$1 { + constructor(name, options) { + this._middlewares = {}; + this.verno = 0; + const deps = Dexie$1.dependencies; + this._options = options = { + addons: Dexie$1.addons, + autoOpen: true, + indexedDB: deps.indexedDB, + IDBKeyRange: deps.IDBKeyRange, + cache: 'cloned', + ...options + }; + this._deps = { + indexedDB: options.indexedDB, + IDBKeyRange: options.IDBKeyRange + }; + const { addons, } = options; + this._dbSchema = {}; + this._versions = []; + this._storeNames = []; + this._allTables = {}; + this.idbdb = null; + this._novip = this; + const state = { + dbOpenError: null, + isBeingOpened: false, + onReadyBeingFired: null, + openComplete: false, + dbReadyResolve: nop, + dbReadyPromise: null, + cancelOpen: nop, + openCanceller: null, + autoSchema: true, + PR1398_maxLoop: 3, + autoOpen: options.autoOpen, + }; + state.dbReadyPromise = new DexiePromise(resolve => { + state.dbReadyResolve = resolve; + }); + state.openCanceller = new DexiePromise((_, reject) => { + state.cancelOpen = reject; + }); + this._state = state; + this.name = name; + this.on = Events(this, "populate", "blocked", "versionchange", "close", { ready: [promisableChain, nop] }); + this.once = (event, callback) => { + const fn = (...args) => { + this.on(event).unsubscribe(fn); + callback.apply(this, args); + }; + return this.on(event, fn); + }; + this.on.ready.subscribe = override(this.on.ready.subscribe, subscribe => { + return (subscriber, bSticky) => { + Dexie$1.vip(() => { + const state = this._state; + if (state.openComplete) { + if (!state.dbOpenError) + DexiePromise.resolve().then(subscriber); + if (bSticky) + subscribe(subscriber); + } + else if (state.onReadyBeingFired) { + state.onReadyBeingFired.push(subscriber); + if (bSticky) + subscribe(subscriber); + } + else { + subscribe(subscriber); + const db = this; + if (!bSticky) + subscribe(function unsubscribe() { + db.on.ready.unsubscribe(subscriber); + db.on.ready.unsubscribe(unsubscribe); + }); + } + }); + }; + }); + this.Collection = createCollectionConstructor(this); + this.Table = createTableConstructor(this); + this.Transaction = createTransactionConstructor(this); + this.Version = createVersionConstructor(this); + this.WhereClause = createWhereClauseConstructor(this); + this.on("versionchange", ev => { + if (ev.newVersion > 0) + console.warn(`Another connection wants to upgrade database '${this.name}'. Closing db now to resume the upgrade.`); + else + console.warn(`Another connection wants to delete database '${this.name}'. Closing db now to resume the delete request.`); + this.close({ disableAutoOpen: false }); + }); + this.on("blocked", ev => { + if (!ev.newVersion || ev.newVersion < ev.oldVersion) + console.warn(`Dexie.delete('${this.name}') was blocked`); + else + console.warn(`Upgrade '${this.name}' blocked by other connection holding version ${ev.oldVersion / 10}`); + }); + this._maxKey = getMaxKey(options.IDBKeyRange); + this._createTransaction = (mode, storeNames, dbschema, parentTransaction) => new this.Transaction(mode, storeNames, dbschema, this._options.chromeTransactionDurability, parentTransaction); + this._fireOnBlocked = ev => { + this.on("blocked").fire(ev); + connections + .filter(c => c.name === this.name && c !== this && !c._state.vcFired) + .map(c => c.on("versionchange").fire(ev)); + }; + this.use(cacheExistingValuesMiddleware); + this.use(cacheMiddleware); + this.use(observabilityMiddleware); + this.use(virtualIndexMiddleware); + this.use(hooksMiddleware); + const vipDB = new Proxy(this, { + get: (_, prop, receiver) => { + if (prop === '_vip') + return true; + if (prop === 'table') + return (tableName) => vipify(this.table(tableName), vipDB); + const rv = Reflect.get(_, prop, receiver); + if (rv instanceof Table) + return vipify(rv, vipDB); + if (prop === 'tables') + return rv.map(t => vipify(t, vipDB)); + if (prop === '_createTransaction') + return function () { + const tx = rv.apply(this, arguments); + return vipify(tx, vipDB); + }; + return rv; + } + }); + this.vip = vipDB; + addons.forEach(addon => addon(this)); + } + version(versionNumber) { + if (isNaN(versionNumber) || versionNumber < 0.1) + throw new exceptions.Type(`Given version is not a positive number`); + versionNumber = Math.round(versionNumber * 10) / 10; + if (this.idbdb || this._state.isBeingOpened) + throw new exceptions.Schema("Cannot add version when database is open"); + this.verno = Math.max(this.verno, versionNumber); + const versions = this._versions; + var versionInstance = versions.filter(v => v._cfg.version === versionNumber)[0]; + if (versionInstance) + return versionInstance; + versionInstance = new this.Version(versionNumber); + versions.push(versionInstance); + versions.sort(lowerVersionFirst); + versionInstance.stores({}); + this._state.autoSchema = false; + return versionInstance; + } + _whenReady(fn) { + return (this.idbdb && (this._state.openComplete || PSD.letThrough || this._vip)) ? fn() : new DexiePromise((resolve, reject) => { + if (this._state.openComplete) { + return reject(new exceptions.DatabaseClosed(this._state.dbOpenError)); + } + if (!this._state.isBeingOpened) { + if (!this._state.autoOpen) { + reject(new exceptions.DatabaseClosed()); + return; + } + this.open().catch(nop); + } + this._state.dbReadyPromise.then(resolve, reject); + }).then(fn); + } + use({ stack, create, level, name }) { + if (name) + this.unuse({ stack, name }); + const middlewares = this._middlewares[stack] || (this._middlewares[stack] = []); + middlewares.push({ stack, create, level: level == null ? 10 : level, name }); + middlewares.sort((a, b) => a.level - b.level); + return this; + } + unuse({ stack, name, create }) { + if (stack && this._middlewares[stack]) { + this._middlewares[stack] = this._middlewares[stack].filter(mw => create ? mw.create !== create : + name ? mw.name !== name : + false); + } + return this; + } + open() { + return usePSD(globalPSD, + () => dexieOpen(this)); + } + _close() { + this.on.close.fire(new CustomEvent('close')); + const state = this._state; + const idx = connections.indexOf(this); + if (idx >= 0) + connections.splice(idx, 1); + if (this.idbdb) { + try { + this.idbdb.close(); + } + catch (e) { } + this.idbdb = null; + } + if (!state.isBeingOpened) { + state.dbReadyPromise = new DexiePromise(resolve => { + state.dbReadyResolve = resolve; + }); + state.openCanceller = new DexiePromise((_, reject) => { + state.cancelOpen = reject; + }); + } + } + close({ disableAutoOpen } = { disableAutoOpen: true }) { + const state = this._state; + if (disableAutoOpen) { + if (state.isBeingOpened) { + state.cancelOpen(new exceptions.DatabaseClosed()); + } + this._close(); + state.autoOpen = false; + state.dbOpenError = new exceptions.DatabaseClosed(); + } + else { + this._close(); + state.autoOpen = this._options.autoOpen || + state.isBeingOpened; + state.openComplete = false; + state.dbOpenError = null; + } + } + delete(closeOptions = { disableAutoOpen: true }) { + const hasInvalidArguments = arguments.length > 0 && typeof arguments[0] !== 'object'; + const state = this._state; + return new DexiePromise((resolve, reject) => { + const doDelete = () => { + this.close(closeOptions); + var req = this._deps.indexedDB.deleteDatabase(this.name); + req.onsuccess = wrap(() => { + _onDatabaseDeleted(this._deps, this.name); + resolve(); + }); + req.onerror = eventRejectHandler(reject); + req.onblocked = this._fireOnBlocked; + }; + if (hasInvalidArguments) + throw new exceptions.InvalidArgument("Invalid closeOptions argument to db.delete()"); + if (state.isBeingOpened) { + state.dbReadyPromise.then(doDelete); + } + else { + doDelete(); + } + }); + } + backendDB() { + return this.idbdb; + } + isOpen() { + return this.idbdb !== null; + } + hasBeenClosed() { + const dbOpenError = this._state.dbOpenError; + return dbOpenError && (dbOpenError.name === 'DatabaseClosed'); + } + hasFailed() { + return this._state.dbOpenError !== null; + } + dynamicallyOpened() { + return this._state.autoSchema; + } + get tables() { + return keys(this._allTables).map(name => this._allTables[name]); + } + transaction() { + const args = extractTransactionArgs.apply(this, arguments); + return this._transaction.apply(this, args); + } + _transaction(mode, tables, scopeFunc) { + let parentTransaction = PSD.trans; + if (!parentTransaction || parentTransaction.db !== this || mode.indexOf('!') !== -1) + parentTransaction = null; + const onlyIfCompatible = mode.indexOf('?') !== -1; + mode = mode.replace('!', '').replace('?', ''); + let idbMode, storeNames; + try { + storeNames = tables.map(table => { + var storeName = table instanceof this.Table ? table.name : table; + if (typeof storeName !== 'string') + throw new TypeError("Invalid table argument to Dexie.transaction(). Only Table or String are allowed"); + return storeName; + }); + if (mode == "r" || mode === READONLY) + idbMode = READONLY; + else if (mode == "rw" || mode == READWRITE) + idbMode = READWRITE; + else + throw new exceptions.InvalidArgument("Invalid transaction mode: " + mode); + if (parentTransaction) { + if (parentTransaction.mode === READONLY && idbMode === READWRITE) { + if (onlyIfCompatible) { + parentTransaction = null; + } + else + throw new exceptions.SubTransaction("Cannot enter a sub-transaction with READWRITE mode when parent transaction is READONLY"); + } + if (parentTransaction) { + storeNames.forEach(storeName => { + if (parentTransaction && parentTransaction.storeNames.indexOf(storeName) === -1) { + if (onlyIfCompatible) { + parentTransaction = null; + } + else + throw new exceptions.SubTransaction("Table " + storeName + + " not included in parent transaction."); + } + }); + } + if (onlyIfCompatible && parentTransaction && !parentTransaction.active) { + parentTransaction = null; + } + } + } + catch (e) { + return parentTransaction ? + parentTransaction._promise(null, (_, reject) => { reject(e); }) : + rejection(e); + } + const enterTransaction = enterTransactionScope.bind(null, this, idbMode, storeNames, parentTransaction, scopeFunc); + return (parentTransaction ? + parentTransaction._promise(idbMode, enterTransaction, "lock") : + PSD.trans ? + usePSD(PSD.transless, () => this._whenReady(enterTransaction)) : + this._whenReady(enterTransaction)); + } + table(tableName) { + if (!hasOwn(this._allTables, tableName)) { + throw new exceptions.InvalidTable(`Table ${tableName} does not exist`); + } + return this._allTables[tableName]; + } +} + +const symbolObservable = typeof Symbol !== "undefined" && "observable" in Symbol + ? Symbol.observable + : "@@observable"; +class Observable { + constructor(subscribe) { + this._subscribe = subscribe; + } + subscribe(x, error, complete) { + return this._subscribe(!x || typeof x === "function" ? { next: x, error, complete } : x); + } + [symbolObservable]() { + return this; + } +} + +let domDeps; +try { + domDeps = { + indexedDB: _global.indexedDB || _global.mozIndexedDB || _global.webkitIndexedDB || _global.msIndexedDB, + IDBKeyRange: _global.IDBKeyRange || _global.webkitIDBKeyRange + }; +} +catch (e) { + domDeps = { indexedDB: null, IDBKeyRange: null }; +} + +function liveQuery(querier) { + let hasValue = false; + let currentValue; + const observable = new Observable((observer) => { + const scopeFuncIsAsync = isAsyncFunction(querier); + function execute(ctx) { + const wasRootExec = beginMicroTickScope(); + try { + if (scopeFuncIsAsync) { + incrementExpectedAwaits(); + } + let rv = newScope(querier, ctx); + if (scopeFuncIsAsync) { + rv = rv.finally(decrementExpectedAwaits); + } + return rv; + } + finally { + wasRootExec && endMicroTickScope(); + } + } + let closed = false; + let abortController; + let accumMuts = {}; + let currentObs = {}; + const subscription = { + get closed() { + return closed; + }, + unsubscribe: () => { + if (closed) + return; + closed = true; + if (abortController) + abortController.abort(); + if (startedListening) + globalEvents.storagemutated.unsubscribe(mutationListener); + }, + }; + observer.start && observer.start(subscription); + let startedListening = false; + const doQuery = () => execInGlobalContext(_doQuery); + function shouldNotify() { + return obsSetsOverlap(currentObs, accumMuts); + } + const mutationListener = (parts) => { + extendObservabilitySet(accumMuts, parts); + if (shouldNotify()) { + doQuery(); + } + }; + const _doQuery = () => { + if (closed || + !domDeps.indexedDB) + { + return; + } + accumMuts = {}; + const subscr = {}; + if (abortController) + abortController.abort(); + abortController = new AbortController(); + const ctx = { + subscr, + signal: abortController.signal, + requery: doQuery, + querier, + trans: null + }; + const ret = execute(ctx); + Promise.resolve(ret).then((result) => { + hasValue = true; + currentValue = result; + if (closed || ctx.signal.aborted) { + return; + } + accumMuts = {}; + currentObs = subscr; + if (!objectIsEmpty(currentObs) && !startedListening) { + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, mutationListener); + startedListening = true; + } + execInGlobalContext(() => !closed && observer.next && observer.next(result)); + }, (err) => { + hasValue = false; + if (!['DatabaseClosedError', 'AbortError'].includes(err?.name)) { + if (!closed) + execInGlobalContext(() => { + if (closed) + return; + observer.error && observer.error(err); + }); + } + }); + }; + setTimeout(doQuery, 0); + return subscription; + }); + observable.hasValue = () => hasValue; + observable.getValue = () => currentValue; + return observable; +} + +const Dexie = Dexie$1; +props(Dexie, { + ...fullNameExceptions, + delete(databaseName) { + const db = new Dexie(databaseName, { addons: [] }); + return db.delete(); + }, + exists(name) { + return new Dexie(name, { addons: [] }).open().then(db => { + db.close(); + return true; + }).catch('NoSuchDatabaseError', () => false); + }, + getDatabaseNames(cb) { + try { + return getDatabaseNames(Dexie.dependencies).then(cb); + } + catch { + return rejection(new exceptions.MissingAPI()); + } + }, + defineClass() { + function Class(content) { + extend(this, content); + } + return Class; + }, + ignoreTransaction(scopeFunc) { + return PSD.trans ? + usePSD(PSD.transless, scopeFunc) : + scopeFunc(); + }, + vip, + async: function (generatorFn) { + return function () { + try { + var rv = awaitIterator(generatorFn.apply(this, arguments)); + if (!rv || typeof rv.then !== 'function') + return DexiePromise.resolve(rv); + return rv; + } + catch (e) { + return rejection(e); + } + }; + }, + spawn: function (generatorFn, args, thiz) { + try { + var rv = awaitIterator(generatorFn.apply(thiz, args || [])); + if (!rv || typeof rv.then !== 'function') + return DexiePromise.resolve(rv); + return rv; + } + catch (e) { + return rejection(e); + } + }, + currentTransaction: { + get: () => PSD.trans || null + }, + waitFor: function (promiseOrFunction, optionalTimeout) { + const promise = DexiePromise.resolve(typeof promiseOrFunction === 'function' ? + Dexie.ignoreTransaction(promiseOrFunction) : + promiseOrFunction) + .timeout(optionalTimeout || 60000); + return PSD.trans ? + PSD.trans.waitFor(promise) : + promise; + }, + Promise: DexiePromise, + debug: { + get: () => debug, + set: value => { + setDebug(value); + } + }, + derive: derive, + extend: extend, + props: props, + override: override, + Events: Events, + on: globalEvents, + liveQuery, + extendObservabilitySet, + getByKeyPath: getByKeyPath, + setByKeyPath: setByKeyPath, + delByKeyPath: delByKeyPath, + shallowClone: shallowClone, + deepClone: deepClone, + getObjectDiff: getObjectDiff, + cmp, + asap: asap$1, + minKey: minKey, + addons: [], + connections: connections, + errnames: errnames, + dependencies: domDeps, + cache, + semVer: DEXIE_VERSION, + version: DEXIE_VERSION.split('.') + .map(n => parseInt(n)) + .reduce((p, c, i) => p + (c / Math.pow(10, i * 2))), +}); +Dexie.maxKey = getMaxKey(Dexie.dependencies.IDBKeyRange); + +if (typeof dispatchEvent !== 'undefined' && typeof addEventListener !== 'undefined') { + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, updatedParts => { + if (!propagatingLocally) { + let event; + event = new CustomEvent(STORAGE_MUTATED_DOM_EVENT_NAME, { + detail: updatedParts + }); + propagatingLocally = true; + dispatchEvent(event); + propagatingLocally = false; + } + }); + addEventListener(STORAGE_MUTATED_DOM_EVENT_NAME, ({ detail }) => { + if (!propagatingLocally) { + propagateLocally(detail); + } + }); +} +function propagateLocally(updateParts) { + let wasMe = propagatingLocally; + try { + propagatingLocally = true; + globalEvents.storagemutated.fire(updateParts); + signalSubscribersNow(updateParts, true); + } + finally { + propagatingLocally = wasMe; + } +} +let propagatingLocally = false; + +let bc; +let createBC = () => { }; +if (typeof BroadcastChannel !== 'undefined') { + createBC = () => { + bc = new BroadcastChannel(STORAGE_MUTATED_DOM_EVENT_NAME); + bc.onmessage = ev => ev.data && propagateLocally(ev.data); + }; + createBC(); + if (typeof bc.unref === 'function') { + bc.unref(); + } + globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, (changedParts) => { + if (!propagatingLocally) { + bc.postMessage(changedParts); + } + }); +} + +if (typeof addEventListener !== 'undefined') { + addEventListener('pagehide', (event) => { + if (!Dexie$1.disableBfCache && event.persisted) { + if (debug) + console.debug('Dexie: handling persisted pagehide'); + bc?.close(); + for (const db of connections) { + db.close({ disableAutoOpen: false }); + } + } + }); + addEventListener('pageshow', (event) => { + if (!Dexie$1.disableBfCache && event.persisted) { + if (debug) + console.debug('Dexie: handling persisted pageshow'); + createBC(); + propagateLocally({ all: new RangeSet(-Infinity, [[]]) }); + } + }); +} + +function add(value) { + return new PropModification({ add: value }); +} + +function remove(value) { + return new PropModification({ remove: value }); +} + +function replacePrefix(a, b) { + return new PropModification({ replacePrefix: [a, b] }); +} + +DexiePromise.rejectionMapper = mapError; +setDebug(debug); + +export { Dexie$1 as Dexie, Entity, PropModification, RangeSet, add, cmp, Dexie$1 as default, liveQuery, mergeRanges, rangesOverlap, remove, replacePrefix }; +//# sourceMappingURL=dexie.mjs.map diff --git a/experimental/javascript-wc-indexeddb/dist/src/components/todo-app/todo-app.component.js b/experimental/javascript-wc-indexeddb/dist/src/components/todo-app/todo-app.component.js new file mode 100644 index 000000000..5cf398eee --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/components/todo-app/todo-app.component.js @@ -0,0 +1,156 @@ +import template from "./todo-app.template.js"; +import { useRouter } from "../../hooks/useRouter.js"; + +import globalStyles from "../../../styles/global.constructable.js"; +import appStyles from "../../../styles/app.constructable.js"; +import mainStyles from "../../../styles/main.constructable.js"; +class TodoApp extends HTMLElement { + #isReady = false; + #numberOfItems = 0; + #numberOfCompletedItems = 0; + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.topbar = node.querySelector("todo-topbar"); + this.list = node.querySelector("todo-list"); + this.bottombar = node.querySelector("todo-bottombar"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, appStyles, mainStyles]; + this.shadow.append(node); + + this.addItem = this.addItem.bind(this); + this.toggleItem = this.toggleItem.bind(this); + this.removeItem = this.removeItem.bind(this); + this.updateItem = this.updateItem.bind(this); + this.toggleItems = this.toggleItems.bind(this); + this.clearCompletedItems = this.clearCompletedItems.bind(this); + this.routeChange = this.routeChange.bind(this); + this.moveToNextPage = this.moveToNextPage.bind(this); + this.moveToPreviousPage = this.moveToPreviousPage.bind(this); + + this.router = useRouter(); + } + + get isReady() { + return this.#isReady; + } + + getInstance() { + return this; + } + + addItem(event) { + const { detail: item } = event; + this.list.addItem(item, this.#numberOfItems++); + this.update(); + } + + toggleItem(event) { + if (event.detail.completed) + this.#numberOfCompletedItems++; + else + this.#numberOfCompletedItems--; + + this.list.toggleItem(event.detail.itemNumber, event.detail.completed); + this.update(); + } + + removeItem(event) { + if (event.detail.completed) + this.#numberOfCompletedItems--; + + this.#numberOfItems--; + this.update(); + this.list.removeItem(event.detail.itemNumber); + } + + updateItem(event) { + this.update(); + } + + toggleItems(event) { + this.list.toggleItems(event.detail.completed); + } + + clearCompletedItems() { + this.list.removeCompletedItems(); + } + + moveToNextPage() { + this.list.moveToNextPage(); + } + + async moveToPreviousPage() { + await this.list.moveToPreviousPage(); + this.bottombar.reenablePreviousPageButton(); + window.dispatchEvent(new CustomEvent("previous-page-loaded", {})); + } + + update() { + const totalItems = this.#numberOfItems; + const completedItems = this.#numberOfCompletedItems; + const activeItems = totalItems - completedItems; + + this.list.setAttribute("total-items", totalItems); + + this.topbar.setAttribute("total-items", totalItems); + this.topbar.setAttribute("active-items", activeItems); + this.topbar.setAttribute("completed-items", completedItems); + + this.bottombar.setAttribute("total-items", totalItems); + this.bottombar.setAttribute("active-items", activeItems); + } + + addListeners() { + this.topbar.addEventListener("toggle-all", this.toggleItems); + this.topbar.addEventListener("add-item", this.addItem); + + this.list.listNode.addEventListener("toggle-item", this.toggleItem); + this.list.listNode.addEventListener("remove-item", this.removeItem); + this.list.listNode.addEventListener("update-item", this.updateItem); + + this.bottombar.addEventListener("clear-completed-items", this.clearCompletedItems); + this.bottombar.addEventListener("next-page", this.moveToNextPage); + this.bottombar.addEventListener("previous-page", this.moveToPreviousPage); + } + + removeListeners() { + this.topbar.removeEventListener("toggle-all", this.toggleItems); + this.topbar.removeEventListener("add-item", this.addItem); + + this.list.listNode.removeEventListener("toggle-item", this.toggleItem); + this.list.listNode.removeEventListener("remove-item", this.removeItem); + this.list.listNode.removeEventListener("update-item", this.updateItem); + + this.bottombar.removeEventListener("clear-completed-items", this.clearCompletedItems); + this.bottombar.removeEventListener("next-page", this.moveToNextPage); + this.bottombar.removeEventListener("previous-page", this.moveToPreviousPage); + } + + routeChange(route) { + const routeName = route.split("/")[1] || "all"; + this.list.updateRoute(routeName); + this.bottombar.updateRoute(routeName); + this.topbar.updateRoute(routeName); + } + + connectedCallback() { + this.update(); + this.addListeners(); + this.router.initRouter(this.routeChange); + this.#isReady = true; + } + + disconnectedCallback() { + this.removeListeners(); + this.#isReady = false; + } +} + +customElements.define("todo-app", TodoApp); + +export default TodoApp; diff --git a/experimental/javascript-wc-indexeddb/dist/src/components/todo-app/todo-app.template.js b/experimental/javascript-wc-indexeddb/dist/src/components/todo-app/todo-app.template.js new file mode 100644 index 000000000..1a55a8194 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/components/todo-app/todo-app.template.js @@ -0,0 +1,14 @@ +const template = document.createElement("template"); + +template.id = "todo-app-template"; +template.innerHTML = ` +
+ +
+ +
+ +
+`; + +export default template; diff --git a/experimental/javascript-wc-indexeddb/dist/src/components/todo-bottombar/todo-bottombar.component.js b/experimental/javascript-wc-indexeddb/dist/src/components/todo-bottombar/todo-bottombar.component.js new file mode 100644 index 000000000..3682d2bf1 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/components/todo-bottombar/todo-bottombar.component.js @@ -0,0 +1,126 @@ +import template from "./todo-bottombar.template.js"; + +import globalStyles from "../../../styles/global.constructable.js"; +import bottombarStyles from "../../../styles/bottombar.constructable.js"; + +const customStyles = new CSSStyleSheet(); +customStyles.replaceSync(` + + .clear-completed-button, .clear-completed-button:active, + .todo-status, + .filter-list + { + position: unset; + transform: unset; + } + + .bottombar { + display: grid; + grid-template-columns: repeat(7, 1fr); + align-items: center; + justify-items: center; + } + + .bottombar > * { + grid-column: span 1; + } + + .filter-list { + grid-column: span 3; + } + + :host([total-items="0"]) > .bottombar { + display: none; + } +`); + +class TodoBottombar extends HTMLElement { + static get observedAttributes() { + return ["total-items", "active-items"]; + } + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.element = node.querySelector(".bottombar"); + this.clearCompletedButton = node.querySelector(".clear-completed-button"); + this.todoStatus = node.querySelector(".todo-status"); + this.filterLinks = node.querySelectorAll(".filter-link"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, bottombarStyles, customStyles]; + this.shadow.append(node); + + this.clearCompletedItems = this.clearCompletedItems.bind(this); + this.MoveToNextPage = this.MoveToNextPage.bind(this); + this.MoveToPreviousPage = this.MoveToPreviousPage.bind(this); + } + + updateDisplay() { + this.todoStatus.textContent = `${this["active-items"]} ${this["active-items"] === "1" ? "item" : "items"} left!`; + } + + updateRoute(route) { + this.filterLinks.forEach((link) => { + if (link.dataset.route === route) + link.classList.add("selected"); + else + link.classList.remove("selected"); + }); + } + + clearCompletedItems() { + this.dispatchEvent(new CustomEvent("clear-completed-items")); + } + + MoveToNextPage() { + this.dispatchEvent(new CustomEvent("next-page")); + } + + MoveToPreviousPage() { + this.element.querySelector(".previous-page-button").disabled = true; + this.dispatchEvent(new CustomEvent("previous-page")); + } + + addListeners() { + this.clearCompletedButton.addEventListener("click", this.clearCompletedItems); + this.element.querySelector(".next-page-button").addEventListener("click", this.MoveToNextPage); + this.element.querySelector(".previous-page-button").addEventListener("click", this.MoveToPreviousPage); + } + + removeListeners() { + this.clearCompletedButton.removeEventListener("click", this.clearCompletedItems); + this.getElementById("next-page-button").removeEventListener("click", this.MoveToNextPage); + this.getElementById("previous-page-button").removeEventListener("click", this.MoveToPreviousPage); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.updateDisplay(); + } + + reenablePreviousPageButton() { + this.element.querySelector(".previous-page-button").disabled = false; + window.dispatchEvent(new CustomEvent("previous-page-button-enabled", {})); + } + + connectedCallback() { + this.updateDisplay(); + this.addListeners(); + } + + disconnectedCallback() { + this.removeListeners(); + } +} + +customElements.define("todo-bottombar", TodoBottombar); + +export default TodoBottombar; diff --git a/experimental/javascript-wc-indexeddb/dist/src/components/todo-bottombar/todo-bottombar.template.js b/experimental/javascript-wc-indexeddb/dist/src/components/todo-bottombar/todo-bottombar.template.js new file mode 100644 index 000000000..e9259fe30 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/components/todo-bottombar/todo-bottombar.template.js @@ -0,0 +1,24 @@ +const template = document.createElement("template"); + +template.id = "todo-bottombar-template"; +template.innerHTML = ` + +`; + +export default template; diff --git a/experimental/javascript-wc-indexeddb/dist/src/components/todo-item/todo-item.component.js b/experimental/javascript-wc-indexeddb/dist/src/components/todo-item/todo-item.component.js new file mode 100644 index 000000000..d663fd242 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/components/todo-item/todo-item.component.js @@ -0,0 +1,182 @@ +import template from "./todo-item.template.js"; +import { useDoubleClick } from "../../hooks/useDoubleClick.js"; +import { useKeyListener } from "../../hooks/useKeyListener.js"; + +import globalStyles from "../../../styles/global.constructable.js"; +import itemStyles from "../../../styles/todo-item.constructable.js"; + +class TodoItem extends HTMLElement { + static get observedAttributes() { + return ["itemid", "itemtitle", "itemcompleted"]; + } + + constructor() { + super(); + + // Renamed this.id to this.itemid and this.title to this.itemtitle. + // When the component assigns to this.id or this.title, this causes the browser's implementation of the existing setters to run, which convert these property sets into internal setAttribute calls. This can have surprising consequences. + // [Issue]: https://github.com/WebKit/Speedometer/issues/313 + this.itemid = ""; + this.itemtitle = "Todo Item"; + this.itemcompleted = "false"; + this.itemIndex = null; + + const node = document.importNode(template.content, true); + this.item = node.querySelector(".todo-item"); + this.toggleLabel = node.querySelector(".toggle-todo-label"); + this.toggleInput = node.querySelector(".toggle-todo-input"); + this.todoText = node.querySelector(".todo-item-text"); + this.todoButton = node.querySelector(".remove-todo-button"); + this.editLabel = node.querySelector(".edit-todo-label"); + this.editInput = node.querySelector(".edit-todo-input"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, itemStyles]; + this.shadow.append(node); + + this.keysListeners = []; + + this.updateItem = this.updateItem.bind(this); + this.toggleItem = this.toggleItem.bind(this); + this.removeItem = this.removeItem.bind(this); + this.startEdit = this.startEdit.bind(this); + this.stopEdit = this.stopEdit.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + + if (window.extraTodoItemCssToAdopt) { + let extraAdoptedStyleSheet = new CSSStyleSheet(); + extraAdoptedStyleSheet.replaceSync(window.extraTodoItemCssToAdopt); + this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet); + } + } + + update(...args) { + args.forEach((argument) => { + switch (argument) { + case "itemid": + if (this.itemid !== undefined) + this.item.id = `todo-item-${this.itemid}`; + break; + case "itemtitle": + if (this.itemtitle !== undefined) { + this.todoText.textContent = this.itemtitle; + this.editInput.value = this.itemtitle; + } + break; + case "itemcompleted": + this.toggleInput.checked = this.itemcompleted === "true"; + break; + } + }); + } + + startEdit() { + this.item.classList.add("editing"); + this.editInput.value = this.itemtitle; + this.editInput.focus(); + } + + stopEdit() { + this.item.classList.remove("editing"); + } + + cancelEdit() { + this.editInput.blur(); + } + + toggleItem() { + // The todo-list checks the "completed" attribute to filter based on route + // (therefore the completed state needs to already be updated before the check) + this.setAttribute("itemcompleted", this.toggleInput.checked); + + this.dispatchEvent( + new CustomEvent("toggle-item", { + detail: { completed: this.toggleInput.checked, itemNumber: this.itemIndex }, + bubbles: true, + }) + ); + } + + removeItem() { + this.dispatchEvent( + new CustomEvent("remove-item", { + detail: { completed: this.toggleInput.checked, itemNumber: this.itemIndex }, + bubbles: true, + }) + ); + this.remove(); + } + + updateItem(event) { + if (event.target.value !== this.itemtitle) { + if (!event.target.value.length) + this.removeItem(); + else + this.setAttribute("itemtitle", event.target.value); + } + + this.cancelEdit(); + } + + addListeners() { + this.toggleInput.addEventListener("change", this.toggleItem); + this.todoText.addEventListener("click", useDoubleClick(this.startEdit, 500)); + this.editInput.addEventListener("blur", this.stopEdit); + this.todoButton.addEventListener("click", this.removeItem); + + this.keysListeners.forEach((listener) => listener.connect()); + } + + removeListeners() { + this.toggleInput.removeEventListener("change", this.toggleItem); + this.todoText.removeEventListener("click", this.startEdit); + this.editInput.removeEventListener("blur", this.stopEdit); + this.todoButton.removeEventListener("click", this.removeItem); + + this.keysListeners.forEach((listener) => listener.disconnect()); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.update(property); + } + + connectedCallback() { + this.update("itemid", "itemtitle", "itemcompleted"); + + this.keysListeners.push( + useKeyListener({ + target: this.editInput, + event: "keyup", + callbacks: { + ["Enter"]: this.updateItem, + ["Escape"]: this.cancelEdit, + }, + }), + useKeyListener({ + target: this.todoText, + event: "keyup", + callbacks: { + [" "]: this.startEdit, // this feels weird + }, + }) + ); + + this.addListeners(); + } + + disconnectedCallback() { + this.removeListeners(); + this.keysListeners = []; + } +} + +customElements.define("todo-item", TodoItem); + +export default TodoItem; diff --git a/experimental/javascript-wc-indexeddb/dist/src/components/todo-item/todo-item.template.js b/experimental/javascript-wc-indexeddb/dist/src/components/todo-item/todo-item.template.js new file mode 100644 index 000000000..9a67675fd --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/components/todo-item/todo-item.template.js @@ -0,0 +1,19 @@ +const template = document.createElement("template"); + +template.id = "todo-item-template"; +template.innerHTML = ` +
  • +
    + + + Placeholder Text + +
    +
    + + +
    +
  • +`; + +export default template; diff --git a/experimental/javascript-wc-indexeddb/dist/src/components/todo-list/todo-list.component.js b/experimental/javascript-wc-indexeddb/dist/src/components/todo-list/todo-list.component.js new file mode 100644 index 000000000..b8cfe026f --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/components/todo-list/todo-list.component.js @@ -0,0 +1,175 @@ +import template from "./todo-list.template.js"; +import TodoItem from "../todo-item/todo-item.component.js"; +import { createStorageManager } from "../../storage/storage-factory.js"; + +import globalStyles from "../../../styles/global.constructable.js"; +import listStyles from "../../../styles/todo-list.constructable.js"; + +const MAX_ON_SCREEN_ITEMS = 10; + +const customListStyles = new CSSStyleSheet(); +customListStyles.replaceSync(` + .todo-list[route="completed"] > [itemcompleted="false"] { + display: none; + } + + .todo-list[route="active"] > [itemcompleted="true"] { + display: none; + } + + :nth-child(${MAX_ON_SCREEN_ITEMS}) ~ todo-item { + display: none; + } +`); + +class TodoList extends HTMLElement { + static get observedAttributes() { + return ["total-items"]; + } + + #route = undefined; + #firstItemIndexOnScreen = 0; + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.listNode = node.querySelector(".todo-list"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, listStyles, customListStyles]; + this.shadow.append(node); + this.classList.add("show-priority"); + this.storageManager = createStorageManager(); + + if (window.extraTodoListCssToAdopt) { + let extraAdoptedStyleSheet = new CSSStyleSheet(); + extraAdoptedStyleSheet.replaceSync(window.extraTodoListCssToAdopt); + this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet); + } + } + + addItem(entry, itemIndex) { + const { id, title, completed } = entry; + const priority = 4 - (itemIndex % 5); + const element = new TodoItem(); + + element.setAttribute("itemid", id); + element.setAttribute("itemtitle", title); + element.setAttribute("itemcompleted", completed); + element.setAttribute("data-priority", priority); + element.itemIndex = itemIndex; + + this.listNode.append(element); + + this.#addItemToStorage(itemIndex, id, title, priority, completed); + } + + removeItem(itemIndex) { + this.storageManager.removeTodo(itemIndex); + } + + addItems(items) { + items.forEach((entry) => this.addItem(entry)); + } + + removeCompletedItems() { + Array.from(this.listNode.children).forEach((element) => { + if (element.itemcompleted === "true") + element.removeItem(); + }); + } + + toggleItems(completed) { + Array.from(this.listNode.children).forEach((element) => { + if (completed && element.itemcompleted === "false") + element.toggleInput.click(); + else if (!completed && element.itemcompleted === "true") + element.toggleInput.click(); + }); + } + + toggleItem(itemNumber, completed) { + // Update the item in the IndexedDB + this.storageManager.toggleTodo(itemNumber, completed); + } + + updateStyles() { + if (parseInt(this["total-items"]) !== 0) + this.listNode.style.display = "block"; + else + this.listNode.style.display = "none"; + } + + updateRoute(route) { + this.#route = route; + switch (route) { + case "completed": + this.listNode.setAttribute("route", "completed"); + break; + case "active": + this.listNode.setAttribute("route", "active"); + break; + default: + this.listNode.setAttribute("route", "all"); + } + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + if (this.isConnected) + this.updateStyles(); + } + + connectedCallback() { + this.updateStyles(); + } + + moveToNextPage() { + for (let i = 0; i < MAX_ON_SCREEN_ITEMS; i++) { + const child = this.listNode.firstChild; + if (!child) + break; + child.remove(); + } + this.#firstItemIndexOnScreen = this.listNode.firstChild.itemIndex; + } + + async moveToPreviousPage() { + const items = await this.storageManager.getTodos(this.#firstItemIndexOnScreen, MAX_ON_SCREEN_ITEMS); + const elements = items.map((item) => { + const { id, title, completed, priority } = item; + const element = new TodoItem(); + element.setAttribute("itemid", id); + element.setAttribute("itemtitle", title); + element.setAttribute("itemcompleted", completed); + element.setAttribute("data-priority", priority); + element.itemIndex = item.itemNumber; + return element; + }); + this.#firstItemIndexOnScreen = items[0].itemNumber; + this.listNode.replaceChildren(...elements); + } + + #addItemToStorage(itemIndex, id, title, priority, completed) { + // Create a todo object with the structure expected by IndexedDB + const todoItem = { + itemNumber: itemIndex, + id, + title, + completed, + priority, + }; + + // Add the item to IndexedDB and handle the Promise + this.storageManager.addTodo(todoItem); + } +} + +customElements.define("todo-list", TodoList); + +export default TodoList; diff --git a/experimental/javascript-wc-indexeddb/dist/src/components/todo-list/todo-list.template.js b/experimental/javascript-wc-indexeddb/dist/src/components/todo-list/todo-list.template.js new file mode 100644 index 000000000..e92320b51 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/components/todo-list/todo-list.template.js @@ -0,0 +1,8 @@ +const template = document.createElement("template"); + +template.id = "todo-list-template"; +template.innerHTML = ` + +`; + +export default template; diff --git a/experimental/javascript-wc-indexeddb/dist/src/components/todo-topbar/todo-topbar.component.js b/experimental/javascript-wc-indexeddb/dist/src/components/todo-topbar/todo-topbar.component.js new file mode 100644 index 000000000..dd7fc505b --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/components/todo-topbar/todo-topbar.component.js @@ -0,0 +1,138 @@ +import template from "./todo-topbar.template.js"; +import { useKeyListener } from "../../hooks/useKeyListener.js"; +import { nanoid } from "../../utils/nanoid.js"; + +import globalStyles from "../../../styles/global.constructable.js"; +import topbarStyles from "../../../styles/topbar.constructable.js"; + +const customListStyles = new CSSStyleSheet(); +customListStyles.replaceSync(` + :host([total-items="0"]) .toggle-all-container { + display: none; + } +`); + +class TodoTopbar extends HTMLElement { + static get observedAttributes() { + return ["active-items", "completed-items"]; + } + + #route = undefined; + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.todoInput = node.querySelector("#new-todo"); + this.toggleInput = node.querySelector("#toggle-all"); + this.toggleContainer = node.querySelector(".toggle-all-container"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, topbarStyles, customListStyles]; + this.shadow.append(node); + + this.keysListeners = []; + + this.toggleAll = this.toggleAll.bind(this); + this.addItem = this.addItem.bind(this); + } + + toggleAll(event) { + this.dispatchEvent( + new CustomEvent("toggle-all", { + detail: { completed: event.target.checked }, + }) + ); + } + + addItem(event) { + if (!event.target.value.length) + return; + + this.dispatchEvent( + new CustomEvent("add-item", { + detail: { + id: nanoid(), + title: event.target.value, + completed: false, + }, + }) + ); + + event.target.value = ""; + } + + updateDisplay() { + if (!parseInt(this["total-items"])) { + this.toggleContainer.style.display = "none"; + return; + } + + this.toggleContainer.style.display = "block"; + + switch (this.#route) { + case "active": + this.toggleInput.checked = false; + this.toggleInput.disabled = !parseInt(this["active-items"]); + break; + case "completed": + this.toggleInput.checked = parseInt(this["completed-items"]); + this.toggleInput.disabled = !parseInt(this["completed-items"]); + break; + default: + this.toggleInput.checked = !parseInt(this["active-items"]); + this.toggleInput.disabled = false; + } + } + + updateRoute(route) { + this.#route = route; + this.updateDisplay(); + } + + addListeners() { + this.toggleInput.addEventListener("change", this.toggleAll); + this.keysListeners.forEach((listener) => listener.connect()); + } + + removeListeners() { + this.toggleInput.removeEventListener("change", this.toggleAll); + this.keysListeners.forEach((listener) => listener.disconnect()); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.updateDisplay(); + } + + connectedCallback() { + this.keysListeners.push( + useKeyListener({ + target: this.todoInput, + event: "keyup", + callbacks: { + ["Enter"]: this.addItem, + }, + }) + ); + + this.updateDisplay(); + this.addListeners(); + this.todoInput.focus(); + } + + disconnectedCallback() { + this.removeListeners(); + this.keysListeners = []; + } +} + +customElements.define("todo-topbar", TodoTopbar); + +export default TodoTopbar; diff --git a/experimental/javascript-wc-indexeddb/dist/src/components/todo-topbar/todo-topbar.template.js b/experimental/javascript-wc-indexeddb/dist/src/components/todo-topbar/todo-topbar.template.js new file mode 100644 index 000000000..e7e5286a3 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/components/todo-topbar/todo-topbar.template.js @@ -0,0 +1,17 @@ +const template = document.createElement("template"); + +template.id = "todo-topbar-template"; +template.innerHTML = ` +
    +
    + + +
    +
    + + +
    +
    +`; + +export default template; diff --git a/experimental/javascript-wc-indexeddb/dist/src/hooks/useDoubleClick.js b/experimental/javascript-wc-indexeddb/dist/src/hooks/useDoubleClick.js new file mode 100644 index 000000000..a1fe952fe --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/hooks/useDoubleClick.js @@ -0,0 +1,19 @@ +/** + * A simple function to normalize a double-click and a double-tab action. + * There is currently no comparable tab action to dblclick. + * + * @param {Function} fn + * @param {number} delay + * @returns + */ +export function useDoubleClick(fn, delay) { + let last = 0; + return function (...args) { + const now = new Date().getTime(); + const difference = now - last; + if (difference < delay && difference > 0) + fn.apply(this, args); + + last = now; + }; +} diff --git a/experimental/javascript-wc-indexeddb/dist/src/hooks/useKeyListener.js b/experimental/javascript-wc-indexeddb/dist/src/hooks/useKeyListener.js new file mode 100644 index 000000000..453747d54 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/hooks/useKeyListener.js @@ -0,0 +1,23 @@ +export function useKeyListener(props) { + const { target, event, callbacks } = props; + + function handleEvent(event) { + Object.keys(callbacks).forEach((key) => { + if (event.key === key) + callbacks[key](event); + }); + } + + function connect() { + target.addEventListener(event, handleEvent); + } + + function disconnect() { + target.removeEventListener(event, handleEvent); + } + + return { + connect, + disconnect, + }; +} diff --git a/experimental/javascript-wc-indexeddb/dist/src/hooks/useRouter.js b/experimental/javascript-wc-indexeddb/dist/src/hooks/useRouter.js new file mode 100644 index 000000000..ab1ab618a --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/hooks/useRouter.js @@ -0,0 +1,43 @@ +/** + * Listens for hash change of the url and calls onChange if available. + * + * @param {Function} callback + * @returns Methods to interact with useRouter. + */ +export const useRouter = (callback) => { + let onChange = callback; + let current = ""; + + /** + * Change event handler. + */ + const handleChange = () => { + current = document.location.hash; + /* istanbul ignore else */ + if (onChange) + onChange(document.location.hash); + }; + + /** + * Initializes router and adds listeners. + * + * @param {Function} callback + */ + const initRouter = (callback) => { + onChange = callback; + window.addEventListener("hashchange", handleChange); + window.addEventListener("load", handleChange); + }; + + /** + * Removes listeners + */ + const disableRouter = () => { + window.removeEventListener("hashchange", handleChange); + window.removeEventListener("load", handleChange); + }; + + const getRoute = () => current.split("/").slice(-1)[0]; + + return { initRouter, getRoute, disableRouter }; +}; diff --git a/experimental/javascript-wc-indexeddb/dist/src/index.mjs b/experimental/javascript-wc-indexeddb/dist/src/index.mjs new file mode 100644 index 000000000..9b8a9abcc --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/index.mjs @@ -0,0 +1,23 @@ +import { BenchmarkConnector } from "./speedometer-utils/benchmark.mjs"; +import suites, { appName, appVersion } from "./workload-test.mjs"; + +window.workloadPromises = {}; +window.workloadPromises.addPromise = new Promise((resolve) => { + window.addEventListener("db-add-completed", () => resolve()); +}); +window.workloadPromises.togglePromise = new Promise((resolve) => { + window.addEventListener("db-toggle-completed", () => resolve()); +}); +window.workloadPromises.deletePromise = new Promise((resolve) => { + window.addEventListener("db-delete-completed", () => resolve()); +}); + +window.addEventListener("db-ready", () => { + /* + Paste below into dev console for manual testing: + window.addEventListener("message", (event) => console.log(event.data)); + window.postMessage({ id: "todomvc-postmessage-1.0.0", key: "benchmark-connector", type: "benchmark-suite", name: "default" }, "*"); + */ + const benchmarkConnector = new BenchmarkConnector(suites, appName, appVersion); + benchmarkConnector.connect(); +}); diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs new file mode 100644 index 000000000..8d842fb9e --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/benchmark.mjs @@ -0,0 +1,131 @@ +/* eslint-disable no-case-declarations */ +import { TestRunner } from "./test-runner.mjs"; +import { Params } from "./params.mjs"; + +/** + * BenchmarkStep + * + * A single test step, with a common interface to interact with. + */ +export class BenchmarkStep { + constructor(name, run, ignoreResult = false) { + this.name = name; + this.run = run; + this.ignoreResult = ignoreResult; + } + + async runAndRecord(params, suite, test, callback) { + const testRunner = new TestRunner(null, null, params, suite, test, callback); + const result = await testRunner.runTest(); + return result; + } +} + +/** + * BenchmarkSuite + * + * A single test suite that contains one or more test steps. + */ +export class BenchmarkSuite { + constructor(name, tests) { + this.name = name; + this.tests = tests; + } + + record(_test, syncTime, asyncTime) { + const total = syncTime + asyncTime; + const results = { + tests: { Sync: syncTime, Async: asyncTime }, + total: total, + }; + + return results; + } + + async runAndRecord(params, onProgress) { + const measuredValues = { + tests: {}, + total: 0, + }; + const suiteStartLabel = `suite-${this.name}-start`; + const suiteEndLabel = `suite-${this.name}-end`; + + performance.mark(suiteStartLabel); + + for (const test of this.tests) { + const result = await test.runAndRecord(params, this, test, this.record); + if (!test.ignoreResult) { + measuredValues.tests[test.name] = result; + measuredValues.total += result.total; + } + onProgress?.(test.name); + } + + performance.mark(suiteEndLabel); + performance.measure(`suite-${this.name}`, suiteStartLabel, suiteEndLabel); + + return { + type: "suite-tests-complete", + status: "success", + result: measuredValues, + suitename: this.name, + }; + } +} + +/** ********************************************************************** + * BenchmarkConnector + * + * postMessage is used to communicate between app and benchmark. + * When the app is ready, an 'app-ready' message is sent to signal that the app can receive instructions. + * + * A prepare script within the apps appends window.name and window.version from the package.json file. + * The appId is build by appending name-version + * It's used as an additional safe-guard to ensure the correct app responds to a message. + *************************************************************************/ +export class BenchmarkConnector { + constructor(suites, name, version) { + this.suites = suites; + this.name = name; + this.version = version; + + if (!name || !version) + console.warn("No name or version supplied, to create a unique appId"); + + this.appId = name && version ? `${name}-${version}` : -1; + this.onMessage = this.onMessage.bind(this); + } + + async onMessage(event) { + if (event.data.id !== this.appId || event.data.key !== "benchmark-connector") + return; + + switch (event.data.type) { + case "benchmark-suite": + const params = new Params(new URLSearchParams(window.location.search)); + const suite = this.suites[event.data.name]; + if (!suite) + console.error(`Suite with the name of "${event.data.name}" not found!`); + const { result } = await suite.runAndRecord(params, (test) => this.sendMessage({ type: "step-complete", status: "success", appId: this.appId, name: this.name, test })); + console.log(result, result.tests); + this.sendMessage({ type: "suite-complete", status: "success", appId: this.appId, result }); + this.disconnect(); + break; + default: + console.error(`Message data type not supported: ${event.data.type}`); + } + } + + sendMessage(message) { + window.top.postMessage(message, "*"); + } + + connect() { + window.addEventListener("message", this.onMessage); + this.sendMessage({ type: "app-ready", status: "success", appId: this.appId }); + } + + disconnect() { + window.removeEventListener("message", this.onMessage); + } +} diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/helpers.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/helpers.mjs new file mode 100644 index 000000000..729d5f3a4 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/helpers.mjs @@ -0,0 +1,30 @@ +/** + * Helper Methods + * + * Various methods that are extracted from the Page class. + */ +export function getParent(lookupStartNode, path) { + lookupStartNode = lookupStartNode.shadowRoot ?? lookupStartNode; + const parent = path.reduce((root, selector) => { + const node = root.querySelector(selector); + return node.shadowRoot ?? node; + }, lookupStartNode); + + return parent; +} + +export function getElement(selector, path = [], lookupStartNode = document) { + const element = getParent(lookupStartNode, path).querySelector(selector); + return element; +} + +export function getAllElements(selector, path = [], lookupStartNode = document) { + const elements = Array.from(getParent(lookupStartNode, path).querySelectorAll(selector)); + return elements; +} + +export function forceLayout() { + const rect = document.body.getBoundingClientRect(); + const e = document.elementFromPoint((rect.width / 2) | 0, (rect.height / 2) | 0); + return e; +} diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs new file mode 100644 index 000000000..701222d97 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/params.mjs @@ -0,0 +1,195 @@ +export class Params { + viewport = { + width: 800, + height: 600, + }; + // Enable a detailed developer menu to change the current Params. + developerMode = false; + startAutomatically = false; + iterationCount = 10; + suites = []; + // A list of tags to filter suites + tags = []; + // Toggle running a dummy suite once before the normal test suites. + useWarmupSuite = false; + // toggle async type vs default raf type. + useAsyncSteps = false; + // Change how a test measurement is triggered and async time is measured: + // "timer": The classic (as in Speedometer 2.x) way using setTimeout + // "raf": Using rAF callbacks, both for triggering the sync part and for measuring async time. + measurementMethod = "raf"; + // Wait time before the sync step in ms. + waitBeforeSync = 0; + // Warmup time before the sync step in ms. + warmupBeforeSync = 0; + // Seed for shuffling the execution order of suites. + // "off": do not shuffle + // "generate": generate a random seed + // : use the provided integer as a seed + shuffleSeed = "off"; + + constructor(searchParams = undefined) { + if (searchParams) + this._copyFromSearchParams(searchParams); + if (!this.developerMode) { + Object.freeze(this.viewport); + Object.freeze(this); + } + } + + _parseInt(value, errorMessage) { + const number = Number(value); + if (!Number.isInteger(number) && errorMessage) + throw new Error(`Invalid ${errorMessage} param: '${value}', expected int.`); + return parseInt(number); + } + + _copyFromSearchParams(searchParams) { + this.viewport = this._parseViewport(searchParams); + this.startAutomatically = this._parseBooleanParam(searchParams, "startAutomatically"); + this.iterationCount = this._parseIntParam(searchParams, "iterationCount", 1); + this.suites = this._parseSuites(searchParams); + this.tags = this._parseTags(searchParams); + this.developerMode = this._parseBooleanParam(searchParams, "developerMode"); + this.useWarmupSuite = this._parseBooleanParam(searchParams, "useWarmupSuite"); + this.useAsyncSteps = this._parseBooleanParam(searchParams, "useAsyncSteps"); + this.waitBeforeSync = this._parseIntParam(searchParams, "waitBeforeSync", 0); + this.warmupBeforeSync = this._parseIntParam(searchParams, "warmupBeforeSync", 0); + this.measurementMethod = this._parseMeasurementMethod(searchParams); + this.shuffleSeed = this._parseShuffleSeed(searchParams); + + const unused = Array.from(searchParams.keys()); + if (unused.length > 0) + console.error("Got unused search params", unused); + } + + _parseBooleanParam(searchParams, paramKey) { + if (!searchParams.has(paramKey)) + return false; + searchParams.delete(paramKey); + return true; + } + + _parseIntParam(searchParams, paramKey, minValue) { + if (!searchParams.has(paramKey)) + return defaultParams[paramKey]; + + const parsedValue = this._parseInt(searchParams.get(paramKey), "waitBeforeSync"); + if (parsedValue < minValue) + throw new Error(`Invalid ${paramKey} param: '${parsedValue}', value must be >= ${minValue}.`); + searchParams.delete(paramKey); + return parsedValue; + } + + _parseViewport(searchParams) { + if (!searchParams.has("viewport")) + return defaultParams.viewport; + const viewportParam = searchParams.get("viewport"); + const [width, height] = viewportParam.split("x"); + const viewport = { + width: this._parseInt(width, "viewport.width"), + height: this._parseInt(height, "viewport.height"), + }; + if (this.viewport.width < 800 || this.viewport.height < 600) + throw new Error(`Invalid viewport param: ${viewportParam}`); + searchParams.delete("viewport"); + return viewport; + } + + _parseSuites(searchParams) { + if (searchParams.has("suite") || searchParams.has("suites")) { + if (searchParams.has("suite") && searchParams.has("suites")) + throw new Error("Params 'suite' and 'suites' can not be used together."); + const value = searchParams.get("suite") || searchParams.get("suites"); + const suites = value.split(","); + if (suites.length === 0) + throw new Error("No suites selected"); + searchParams.delete("suite"); + searchParams.delete("suites"); + return suites; + } + return defaultParams.suites; + } + + _parseTags(searchParams) { + if (!searchParams.has("tags")) + return defaultParams.tags; + if (this.suites.length) + throw new Error("'suites' and 'tags' cannot be used together."); + const tags = searchParams.get("tags").split(","); + searchParams.delete("tags"); + return tags; + } + + _parseMeasurementMethod(searchParams) { + if (!searchParams.has("measurementMethod")) + return defaultParams.measurementMethod; + const measurementMethod = searchParams.get("measurementMethod"); + if (measurementMethod !== "raf") + throw new Error(`Invalid measurement method: '${measurementMethod}', must be 'raf'.`); + searchParams.delete("measurementMethod"); + return measurementMethod; + } + + _parseShuffleSeed(searchParams) { + if (!searchParams.has("shuffleSeed")) + return defaultParams.shuffleSeed; + let shuffleSeed = searchParams.get("shuffleSeed"); + if (shuffleSeed !== "off") { + if (shuffleSeed === "generate") { + shuffleSeed = Math.floor((Math.random() * 1) << 16); + console.log(`Generated a random suite order seed: ${shuffleSeed}`); + } else { + shuffleSeed = parseInt(shuffleSeed); + } + if (!Number.isInteger(shuffleSeed)) + throw new Error(`Invalid shuffle seed: '${shuffleSeed}', must be either 'off', 'generate' or an integer.`); + } + searchParams.delete("shuffleSeed"); + return shuffleSeed; + } + + toCompleteSearchParamsObject() { + return this.toSearchParamsObject(false); + } + + toSearchParamsObject(filter = true) { + const rawUrlParams = { __proto__: null }; + for (const [key, value] of Object.entries(this)) { + // Handle composite values separately. + if (key === "viewport" || key === "suites" || key === "tags") + continue; + // Skip over default values. + if (filter && value === defaultParams[key]) + continue; + rawUrlParams[key] = value; + } + + if (this.viewport.width !== defaultParams.viewport.width || this.viewport.height !== defaultParams.viewport.height) + rawUrlParams.viewport = `${this.viewport.width}x${this.viewport.height}`; + + if (this.suites.length) + rawUrlParams.suites = this.suites.join(","); + else if (this.tags.length) + rawUrlParams.tags = this.tags.join(","); + + return new URLSearchParams(rawUrlParams); + } + + toSearchParams() { + return this.toSearchParamsObject().toString(); + } +} + +export const defaultParams = new Params(); + +let maybeCustomParams = defaultParams; +if (globalThis?.location?.search) { + const searchParams = new URLSearchParams(globalThis.location.search); + try { + maybeCustomParams = new Params(searchParams); + } catch (e) { + console.error("Invalid URL Param", e, "\nUsing defaults as fallback:", maybeCustomParams); + } +} +export const params = maybeCustomParams; diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs new file mode 100644 index 000000000..e83a95ba8 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-invoker.mjs @@ -0,0 +1,86 @@ +class TestInvoker { + constructor(syncCallback, asyncCallback, reportCallback, params) { + this._syncCallback = syncCallback; + this._asyncCallback = asyncCallback; + this._reportCallback = reportCallback; + this._params = params; + } +} + +class BaseRAFTestInvoker extends TestInvoker { + start() { + return new Promise((resolve) => { + if (this._params.waitBeforeSync) + setTimeout(() => this._scheduleCallbacks(resolve), this._params.waitBeforeSync); + else + this._scheduleCallbacks(resolve); + }); + } +} + +class RAFTestInvoker extends BaseRAFTestInvoker { + _scheduleCallbacks(resolve) { + requestAnimationFrame(() => this._syncCallback()); + requestAnimationFrame(() => { + setTimeout(() => { + this._asyncCallback(); + setTimeout(async () => { + const result = await this._reportCallback(); + resolve(result); + }, 0); + }, 0); + }); + } +} + +class AsyncRAFTestInvoker extends BaseRAFTestInvoker { + static mc = new MessageChannel(); + _scheduleCallbacks(resolve) { + let gotTimer = false; + let gotMessage = false; + let gotPromise = false; + + const tryTriggerAsyncCallback = () => { + if (!gotTimer || !gotMessage || !gotPromise) + return; + + this._asyncCallback(); + setTimeout(async () => { + const results = await this._reportCallback(); + resolve(results); + }, 0); + }; + + requestAnimationFrame(async () => { + await this._syncCallback(); + gotPromise = true; + tryTriggerAsyncCallback(); + }); + + requestAnimationFrame(() => { + setTimeout(async () => { + await Promise.resolve(); + gotTimer = true; + tryTriggerAsyncCallback(); + }); + + AsyncRAFTestInvoker.mc.port1.addEventListener( + "message", + async function () { + await Promise.resolve(); + gotMessage = true; + tryTriggerAsyncCallback(); + }, + { once: true } + ); + AsyncRAFTestInvoker.mc.port1.start(); + AsyncRAFTestInvoker.mc.port2.postMessage("speedometer"); + }); + } +} + +export const TEST_INVOKER_LOOKUP = { + __proto__: null, + raf: RAFTestInvoker, + async: AsyncRAFTestInvoker, +}; diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs new file mode 100644 index 000000000..52defcb36 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/test-runner.mjs @@ -0,0 +1,112 @@ +import { TEST_INVOKER_LOOKUP } from "./test-invoker.mjs"; + +export class TestRunner { + #frame; + #page; + #params; + #suite; + #test; + #callback; + #type; + + constructor(frame, page, params, suite, test, callback, type) { + this.#suite = suite; + this.#test = test; + this.#params = params; + this.#callback = callback; + this.#page = page; + this.#frame = frame; + this.#type = type; + } + + get page() { + return this.#page; + } + + get test() { + return this.#test; + } + + _runSyncStep(test, page) { + test.run(page); + } + + async runTest() { + // Prepare all mark labels outside the measuring loop. + const suiteName = this.#suite.name; + const testName = this.#test.name; + const syncStartLabel = `${suiteName}.${testName}-start`; + const syncEndLabel = `${suiteName}.${testName}-sync-end`; + const asyncEndLabel = `${suiteName}.${testName}-async-end`; + + let syncTime; + let asyncStartTime; + let asyncTime; + + const runSync = async () => { + if (this.#params.warmupBeforeSync) { + performance.mark("warmup-start"); + const startTime = performance.now(); + // Infinite loop for the specified ms. + while (performance.now() - startTime < this.#params.warmupBeforeSync) + continue; + performance.mark("warmup-end"); + } + performance.mark(syncStartLabel); + const syncStartTime = performance.now(); + + if (this.#type === "async") + await this._runSyncStep(this.test, this.page); + else + this._runSyncStep(this.test, this.page); + + const mark = performance.mark(syncEndLabel); + const syncEndTime = mark.startTime; + + syncTime = syncEndTime - syncStartTime; + asyncStartTime = syncEndTime; + }; + const measureAsync = () => { + const bodyReference = this.#frame ? this.#frame.contentDocument.body : document.body; + const windowReference = this.#frame ? this.#frame.contentWindow : window; + // Some browsers don't immediately update the layout for paint. + // Force the layout here to ensure we're measuring the layout time. + const height = bodyReference.getBoundingClientRect().height; + windowReference._unusedHeightValue = height; // Prevent dead code elimination. + + const asyncEndTime = performance.now(); + performance.mark(asyncEndLabel); + + asyncTime = asyncEndTime - asyncStartTime; + + if (this.#params.warmupBeforeSync) + performance.measure("warmup", "warmup-start", "warmup-end"); + performance.measure(`${suiteName}.${testName}-sync`, syncStartLabel, syncEndLabel); + performance.measure(`${suiteName}.${testName}-async`, syncEndLabel, asyncEndLabel); + }; + + const report = () => this.#callback(this.#test, syncTime, asyncTime); + const invokerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod; + const invokerClass = TEST_INVOKER_LOOKUP[invokerType]; + const invoker = new invokerClass(runSync, measureAsync, report, this.#params); + + return invoker.start(); + } +} + +export class AsyncTestRunner extends TestRunner { + constructor(frame, page, params, suite, test, callback, type) { + super(frame, page, params, suite, test, callback, type); + } + + async _runSyncStep(test, page) { + await test.run(page); + } +} + +export const TEST_RUNNER_LOOKUP = { + __proto__: null, + default: TestRunner, + async: AsyncTestRunner, + remote: TestRunner, +}; diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/todomvc-utils.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/todomvc-utils.mjs new file mode 100644 index 000000000..dca451575 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/todomvc-utils.mjs @@ -0,0 +1 @@ +export const numberOfItemsToAdd = 100; diff --git a/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/translations.mjs b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/translations.mjs new file mode 100644 index 000000000..a8ce5ddbe --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/speedometer-utils/translations.mjs @@ -0,0 +1,734 @@ +export const todos = { + en: [ + "Electronic Granite Hat", + "Bespoke Soft Table", + "Ergonomic Fresh Bike", + "Luxurious Wooden Cheese", + "Gorgeous Fresh Pizza", + "Rustic Rubber Shirt", + "Modern Rubber Soap", + "Small Bronze Ball", + "Awesome Bronze Shoes", + "Bespoke Steel Chair", + "Practical Plastic Soap", + "Incredible Granite Bacon", + "Elegant Metal Keyboard", + "Electronic Wooden Sausages", + "Tasty Wooden Gloves", + "Luxurious Metal Cheese", + "Awesome Rubber Gloves", + "Sleek Soft Car", + "Licensed Fresh Salad", + "Ergonomic Frozen Towels", + "Modern Rubber Keyboard", + "Tasty Concrete Pizza", + "Handmade Plastic Chicken", + "Luxurious Rubber Chicken", + "Practical Soft Fish", + "Ergonomic Bronze Shirt", + "Handcrafted Plastic Bacon", + "Unbranded Plastic Pants", + "Modern Wooden Sausages", + "Handmade Steel Shoes", + "Rustic Steel Bike", + "Gorgeous Frozen Salad", + "Handmade Bronze Chicken", + "Sleek Granite Bike", + "Generic Concrete Sausages", + "Incredible Plastic Tuna", + "Bespoke Fresh Cheese", + "Electronic Fresh Bacon", + "Licensed Wooden Car", + "Recycled Fresh Fish", + "Incredible Fresh Shoes", + "Practical Soft Chips", + "Small Soft Chicken", + "Intelligent Fresh Mouse", + "Modern Metal Mouse", + "Tasty Granite Gloves", + "Awesome Rubber Bike", + "Small Steel Shirt", + "Refined Concrete Computer", + "Sleek Frozen Shirt", + "Intelligent Concrete Shoes", + "Handmade Rubber Car", + "Sleek Rubber Towels", + "Unbranded Concrete Hat", + "Incredible Plastic Fish", + "Practical Soft Gloves", + "Organic Stone Pizza", + "Generic Wooden Keyboard", + "Recycled Wooden Chips", + "Incredible Rubber Chips", + "Ergonomic Granite Shirt", + "Tasty Frozen Keyboard", + "Gorgeous Steel Soap", + "Luxurious Plastic Chair", + "Elegant Frozen Bike", + "Recycled Steel Chair", + "Modern Bronze Sausages", + "Elegant Wooden Cheese", + "Small Plastic Sausages", + "Luxurious Frozen Shoes", + "Sleek Plastic Sausages", + "Handcrafted Fresh Sausages", + "Incredible Soft Chair", + "Recycled Wooden Soap", + "Soft Rubber Duck", + "Licensed Concrete Tuna", + "Luxurious Granite Pants", + "Refined Rubber Keyboard", + "Handcrafted Plastic Computer", + "Practical Steel Salad", + "Incredible Soft Bacon", + "Practical Metal Fish", + "Elegant Rubber Shirt", + "Handcrafted Rubber Table", + "Gorgeous Wooden Table", + "Fantastic Steel Sausages", + "Small Soft Keyboard", + "Generic Steel Ball", + "Electronic Frozen Hat", + "Gorgeous Fresh Chair", + "Sleek Soft Sausages", + "Gorgeous Wooden Towels", + "Bespoke Granite Pizza", + "Generic Metal Salad", + "Handmade Rubber Cheese", + "Fantastic Steel Chair", + "Handcrafted Frozen Computer", + "Rustic Rubber Mouse", + "Sleek Granite Pizza", + "Gorgeous Plastic Keyboard", + ], + "zh-cn": [ + "电子花岗岩帽子", + "定制软桌", + "符合人体工程学新鲜自行车", + "豪华的木制奶酪", + "华丽的新鲜披萨", + "质朴的橡胶衬衫", + "现代橡胶肥皂", + "小青铜球", + "很棒的青铜鞋", + "定制钢椅", + "实用的塑料肥皂", + "令人难以置信的花岗岩培根", + "优雅的金属键盘", + "电子木香肠", + "美味的木制手套", + "豪华的金属奶酪", + "很棒的橡胶手套", + "光滑的柔软汽车", + "有执照的新鲜沙拉", + "人体工程学的冷冻毛巾", + "现代橡胶键盘", + "美味的混凝土披萨", + "手工塑料鸡", + "豪华橡胶鸡", + "实用的软鱼", + "人体工程学的青铜衬衫", + "手工制作的塑料培根", + "未品牌的塑料裤", + "现代木香肠", + "手工钢鞋", + "质朴的钢自行车", + "华丽的冷冻沙拉", + "手工铜鸡", + "光滑的花岗岩自行车", + "通用混凝土香肠", + "令人难以置信的塑料金枪鱼", + "定制新鲜奶酪", + "电子新鲜培根", + "有执照的木车", + "回收的新鲜鱼", + "令人难以置信的新鲜鞋", + "实用的软芯片", + "小鸡肉", + "智能新鲜鼠标", + "现代金属鼠标", + "美味的花岗岩手套", + "很棒的橡胶自行车", + "小钢衬衫", + "精制混凝土计算机", + "光滑的冷冻衬衫", + "智能混凝土鞋", + "手工橡胶车", + "光滑的橡胶毛巾", + "未品牌的混凝土帽子", + "令人难以置信的塑料鱼", + "实用的软手套", + "有机石比萨", + "通用木制键盘", + "再生木芯片", + "令人难以置信的橡胶芯片", + "人体工程学花岗岩衬衫", + "美味的冷冻键盘", + "华丽的钢肥皂", + "豪华的塑料椅", + "优雅的冷冻自行车", + "再生钢椅", + "现代铜香肠", + "优雅的木制奶酪", + "小塑料香肠", + "豪华的冷冻鞋", + "光滑的塑料香肠", + "手工制作的新鲜香肠", + "令人难以置信的柔软椅子", + "回收的木制肥皂", + "软橡皮鸭", + "许可的混凝土金枪鱼", + "豪华的花岗岩裤", + "精制橡胶键盘", + "手工制作的塑料计算机", + "实用的钢沙拉", + "令人难以置信的软培根", + "实用的金属鱼", + "优雅的橡胶衬衫", + "手工橡胶桌", + "华丽的木桌", + "奇妙的钢香肠", + "小软键盘", + "通用钢球", + "电子冷冻帽子", + "华丽的新鲜椅子", + "光滑的软香肠", + "华丽的木制毛巾", + "定制的花岗岩披萨", + "通用金属沙拉", + "手工橡胶奶酪", + "奇妙的钢椅", + "手工制作的冷冻计算机", + "质朴的橡胶老鼠", + "光滑的花岗岩披萨", + "华丽的塑料键盘", + ], + ja: [ + "電子花崗岩の帽子", + "オーダーメイドのソフトテーブル", + "人間工学に基づいた新鮮な自転車", + "豪華な木製チーズ", + "ゴージャスな新鮮なピザ", + "素朴なラバーシャツ", + "モダンゴム石鹸", + "小さなブロンズボール", + "素晴らしいブロンズシューズ", + "オーダーメイドのスチールチェア", + "実用的なプラスチック石鹸", + "信じられないほどの花崗岩のベーコン", + "エレガントなメタルキーボード", + "電子木製ソーセージ", + "おいしい木製手袋", + "豪華なメタルチーズ", + "素晴らしいラバーグローブ", + "洗練されたソフトカー", + "ライセンスされた新鮮なサラダ", + "人間工学に基づいた冷凍タオル", + "モダンなゴムキーボード", + "おいしいコンクリートピザ", + "手作りのプラスチックチキン", + "豪華なゴム鶏", + "実用的な柔らかい魚", + "人間工学に基づいたブロンズシャツ", + "手作りのプラスチックベーコン", + "ブランドのないプラスチックパンツ", + "モダンな木製ソーセージ", + "手作りのスチールシューズ", + "素朴なスチールバイク", + "ゴージャスな冷凍サラダ", + "手作りのブロンズチキン", + "洗練された花崗岩の自転車", + "一般的なコンクリートソーセージ", + "信じられないほどのプラスチックマグロ", + "オーダーメイドのフレッシュチーズ", + "電子新鮮なベーコン", + "ライセンスされた木製車", + "新鮮な魚をリサイクルしました", + "信じられないほどの新鮮な靴", + "実用的なソフトチップ", + "小さな柔らかい鶏", + "インテリジェントな新鮮なマウス", + "現代の金属マウス", + "おいしい花崗岩の手袋", + "素晴らしいラバーバイク", + "小さなスチールシャツ", + "洗練されたコンクリートコンピューター", + "洗練された冷凍シャツ", + "インテリジェントコンクリートシューズ", + "手作りのラバーカー", + "洗練されたゴムタオル", + "ブランドのないコンクリートの帽子", + "信じられないほどのプラスチックの魚", + "実用的なソフトグローブ", + "オーガニックストーンピザ", + "一般的な木製キーボード", + "リサイクルされた木製チップ", + "信じられないほどのゴムチップ", + "人間工学に基づいた花崗岩のシャツ", + "おいしい冷凍キーボード", + "ゴージャスなスチールソープ", + "豪華なプラスチック製の椅子", + "エレガントな冷凍自転車", + "リサイクルスチールチェア", + "モダンブロンズソーセージ", + "エレガントな木製チーズ", + "小さなプラスチックソーセージ", + "豪華な冷凍靴", + "なめらかなプラスチックソーセージ", + "手作りの新鮮なソーセージ", + "信じられないほどのソフトチェア", + "リサイクルされた木製石鹸", + "柔らかいラバーアヒル", + "ライセンスされたコンクリートマグロ", + "豪華な花崗岩のズボン", + "洗練されたゴムキーボード", + "手作りのプラスチックコンピューター", + "実用的なスチールサラダ", + "信じられないほどのソフトベーコン", + "実用的な金属魚", + "エレガントなラバーシャツ", + "手作りのゴムテーブル", + "ゴージャスな木製のテーブル", + "素晴らしいスチールソーセージ", + "小さなソフトキーボード", + "ジェネリックスチールボール", + "電子冷凍帽子", + "ゴージャスな新鮮な椅子", + "洗練された柔らかいソーセージ", + "ゴージャスな木製タオル", + "オーダーメイドの花崗岩のピザ", + "ジェネリックメタルサラダ", + "手作りのゴムチーズ", + "素晴らしいスチールチェア", + "手作りの冷凍コンピューター", + "素朴なゴムマウス", + "洗練された花崗岩のピザ", + "ゴージャスなプラスチックキーボード", + ], + es: [ + "Sombrero de granito electrónico", + "Mesa suave a medida", + "Bicicleta ergonómica fresca", + "Lujoso queso de madera", + "Hermosa pizza fresca", + "Camisa de goma rústica", + "Jabón de goma moderno", + "Bola de bronce pequeña", + "Zapatos de bronce impresionantes", + "Silla de acero a medida", + "Jabón de plástico práctico", + "Tocino de granito increíble", + "Teclado de metal elegante", + "Salchichas de madera electrónica", + "Sabrosos guantes de madera", + "Lujoso queso de metal", + "Guantes de goma impresionantes", + "Coche suave y elegante", + "Ensalada fresca con licencia", + "Toallas congeladas ergonómicas", + "Teclado de goma moderno", + "Pizza de hormigón sabrosa", + "Pollo de plástico hecho a mano", + "Pollo de goma lujoso", + "Pescado suave práctico", + "Camisa de bronce ergonómica", + "Tocino de plástico artesanal", + "Pantalones de plástico sin marca", + "Salchichas de madera modernas", + "Zapatos de acero hechos a mano", + "Bicicleta de acero rústica", + "Hermosa ensalada congelada", + "Pollo bronce hecho a mano", + "Bicicleta de granito elegante", + "Salchichas de concreto genérico", + "Atún de plástico increíble", + "Queso fresco a medida", + "Tocino fresco electrónico", + "Coche de madera con licencia", + "Pescado reciclado", + "Increíbles zapatos frescos", + "Chips suaves prácticos", + "Pollo pequeño y suave", + "Ratón fresco inteligente", + "Ratón de metal moderno", + "Sabrosos guantes de granito", + "Impresionante bicicleta de goma", + "Camisa de acero pequeña", + "Ordenador de concreto refinada", + "Camisa elegante congelada", + "Zapatos de concreto inteligentes", + "Coche de goma hecho a mano", + "Toallas de goma elegantes", + "Sombrero de hormigón sin marca", + "Pescado de plástico increíble", + "Guantes suaves prácticos", + "Pizza de piedra orgánica", + "Teclado de madera genérico", + "Chips de madera reciclados", + "Increíbles chips de goma", + "Camisa de granito ergonómico", + "Sabroso teclado congelado", + "Hermoso jabón de acero", + "Lujosa silla de plástico", + "Bicicleta congelada elegante", + "Silla de acero reciclada", + "Salchichas de bronce modernas", + "Elegante queso de madera", + "Pequeñas salchichas de plástico", + "Zapatos congelados lujosos", + "Salchichas de plástico elegantes", + "Salchichas frescas artesanales", + "Increíble silla suave", + "Jabón de madera reciclado", + "Pato de goma suave", + "Atún de hormigón con licencia", + "Pantalones de granito lujosos", + "Teclado de goma refinado", + "Computadora de plástico hecha a mano", + "Ensalada de acero práctica", + "Bocino suave increíble", + "Pescado de metal práctico", + "Elegante camisa de goma", + "Mesa de goma artesanal", + "Hermosa mesa de madera", + "Fantásticas salchichas de acero", + "Pequeño teclado suave", + "Bola de acero genérica", + "Sombrero de congelado", + "Hermosa silla fresca", + "Salchichas suaves y elegantes", + "Hermosas toallas de madera", + "Pizza de granito a medida", + "Ensalada de metal genérica", + "Queso de goma hecho a mano", + "Fantástica silla de acero", + "Computadora congelada hecha a mano", + "Ratón de goma rústica", + "Pizza de granito elegante", + "Hermoso teclado de plástico", + ], + de: [ + "Elektronischer Granithut", + "Maßgeschneiderter weicher Tisch", + "Ergonomisches frisches Fahrrad", + "Luxuriöser Holzkäse", + "Wunderschöne frische Pizza", + "Rustikales Gummihemd", + "Moderne Gummiseife", + "Kleine Bronzekugel", + "Tolle Bronzeschuhe", + "Maßgeschneiderter Stahlstuhl", + "Praktische Plastikseife", + "Unglaublicher Granitspeck", + "Elegante Metall -Tastatur", + "Elektronische Holzwurst", + "Leckere Holzhandschuhe", + "Luxuriöser Metallkäse", + "Super Gummihandschuhe", + "Schlankes weiches Auto", + "Lizenzierter frischer Salat", + "Ergonomische gefrorene Handtücher", + "Moderne Gummi Tastatur", + "Leckere Betonpizza", + "Handgefertigtes Plastikhuhn", + "Luxuriöses Gummihähnchen", + "Praktischer weicher Fisch", + "Ergonomisches Bronzehemd", + "Handgefertigter Plastikspeck", + "Plastikhosen ohne Marken", + "Moderne Holz Würste", + "Handgefertigte Stahlschuhe", + "Rustikales Stahlrad", + "Wunderschöner gefrorener Salat", + "Handgefertigtes Bronze Hühnchen", + "Schlankes Granitrad", + "Generische Beton Würste", + "Unglaublicher Plastik Thunfisch", + "Maßgeschneiderter frischer Käse", + "Elektronischer frischer Speck", + "Lizenziertes Holzauto", + "Recycelter frischer Fisch", + "Unglaubliche frische Schuhe", + "Praktische weiche Chips", + "Kleines weiches Huhn", + "Intelligente frische Maus", + "Moderne Metallmaus", + "Leckere Granithandschuhe", + "Super Gummi Fahrrad", + "Kleines Stahlhemd", + "Raffinierter Betoncomputer", + "Schlankes gefrorenes Hemd", + "Intelligente Betonschuhe", + "Handgefertigtes Gummiauto", + "Schlanke Gummi Handtücher", + "Betonhut ohne Markenzeichen", + "Unglaublicher Plastikfisch", + "Praktische weiche Handschuhe", + "Bio Steinpizza", + "Generische hölzerne Tastatur", + "Recycelte Holzchips", + "Unglaubliche Gummischchips", + "Ergonomisches Granithemd", + "Leckere gefrorene Tastatur", + "Wunderschöne Stahlseife", + "Luxuriöser Plastikstuhl", + "Elegantes gefrorenes Fahrrad", + "Recycelter Stahlstuhl", + "Moderne Bronze Würste", + "Eleganter Holzkäse", + "Kleine Plastikwurst", + "Luxuriöse gefrorene Schuhe", + "Schlanke Plastikwurst", + "Handgefertigte frische Würste", + "Unglaublicher weicher Stuhl", + "Recycelte Holzseife", + "Weiche Gummi Ente", + "Lizenzierter Beton Thunfisch", + "Luxuriöse Granithosen", + "Raffinierte Gummi Tastatur", + "Handgefertigter Kunststoffcomputer", + "Praktischer Stahlsalat", + "Unglaublicher weicher Speck", + "Praktische Metallfische", + "Elegantes Gummihemd", + "Handgefertigter Gummi Tisch", + "Wunderschöner Holztisch", + "Fantastische Stahlwurst", + "Kleine weiche Tastatur", + "Generischer Stahlkugel", + "Elektronischer Gefrorenhut", + "Wunderschöner frischer Stuhl", + "Schlanke weiche Würste", + "Wunderschöne Holztücher", + "Maßgeschneiderte Granitpizza", + "Generischer Metallsalat", + "Handgefertigter Gummi Käse", + "Fantastischer Stahlstuhl", + "Handgefertigter gefrorener Computer", + "Rustikale Gummi Maus", + "Schlanke Granitpizza", + "Wunderschöne Plastiktastatur", + ], + ru: [ + "Электронная гранитная шляпа", + "Беспокойный мягкий стол", + "Эргономичный свежий велосипед", + "Роскошный деревянный сыр", + "Великолепная свежая пицца", + "Деревенская резиновая рубашка", + "Современное резиновое мыло", + "Маленький бронзовый мяч", + "Потрясающие бронзовые туфли", + "Стальное кресло на заказ", + "Практическое пластиковое мыло", + "Невероятный гранитный бекон", + "Элегантная металлическая клавиатура", + "Электронные деревянные колбасы", + "Вкусные деревянные перчатки", + "Роскошный металлический сыр", + "Потрясающие резиновые перчатки", + "Гладкая мягкая машина", + "Лицензированный свежий салат", + "Эргономичные замороженные полотенца", + "Современная резиновая клавиатура", + "Вкусная бетонная пицца", + "Пластиковая курица ручной работы", + "Роскошная резиновая курица", + "Практическая мягкая рыба", + "Эргономичная бронзовая рубашка", + "Пластиковый бекон из ручной работы", + "Непревзойденные пластиковые брюки", + "Современные деревянные колбасы", + "Стальные туфли ручной работы", + "Деревенский стальный велосипед", + "Великолепный замороженный салат", + "Бронзовая курица ручной работы", + "Гладкий гранитный велосипед", + "Общие бетонные колбасы", + "Невероятный пластиковый тунец", + "Созданный свежий сыр", + "Электронный свежий бекон", + "Лицензированный деревянный автомобиль", + "Переработанная свежая рыба", + "Невероятные свежие туфли", + "Практические мягкие чипсы", + "Маленькая мягкая курица", + "Интеллектуальная свежая мышь", + "Современная металлическая мышь", + "Вкусные гранитные перчатки", + "Потрясающий резиновый велосипед", + "Маленькая стальная рубашка", + "Изысканный бетонный компьютер", + "Гладкая замороженная рубашка", + "Интеллектуальные бетонные туфли", + "Резиновый автомобиль ручной работы", + "Гладкие резиновые полотенца", + "Непревзойденная бетонная шляпа", + "Невероятная пластиковая рыба", + "Практические мягкие перчатки", + "Органическая камня пицца", + "Общая деревянная клавиатура", + "Переработанные деревянные чипсы", + "Невероятные резиновые чипсы", + "Эргономичная гранитная рубашка", + "Вкусная замороженная клавиатура", + "Великолепное стальное мыло", + "Роскошное пластиковое кресло", + "Элегантный замороженный велосипед", + "Переработанное стальное кресло", + "Современные бронзовые колбасы", + "Элегантный деревянный сыр", + "Маленькие пластиковые колбаски", + "Роскошные замороженные туфли", + "Гладкие пластиковые колбаски", + "Свежие колбаски ручной работы", + "Невероятный мягкий стул", + "Переработанное деревянное мыло", + "Мягкая резиновая утка", + "Лицензированный бетонный тунец", + "Роскошные гранитные штаны", + "Рафинированная резиновая клавиатура", + "Пластиковый компьютер ручной работы", + "Практический стальной салат", + "Невероятный мягкий бекон", + "Практическая металлическая рыба", + "Элегантная резиновая рубашка", + "Ручной резиновый стол", + "Великолепный деревянный стол", + "Фантастические стальные колбаски", + "Небольшая мягкая клавиатура", + "Общий стальной шар", + "Электронная замороженная шляпа", + "Великолепный свежий стул", + "Гладкие мягкие колбаски", + "Великолепные деревянные полотенца", + "Сделанная на заказ гранитная пицца", + "Общий металлический салат", + "Резиновый сыр ручной работы", + "Фантастическое стальное кресло", + "Замороженный компьютер ручной работы", + "Деревенская резиновая мышь", + "Главная гранитная пицца", + "Великолепная пластиковая клавиатура", + ], + emoji: [ + "Electronic Granite Hat 👆🏻", + "Bespoke Soft 🍷 Table", + "Ergonomic Fresh Bike 😚😚", + "Luxurious 🍉 Wooden Cheese 🍮", + "Gorgeous Fresh Pizza ⛔", + "Rustic 💪🏽 Rubber Shirt", + "Modern Rubber 🍀 Soap", + "👍 Small Bronze Ball 👍", + "Awesome Bronze Shoes 😎", + "Bespoke 👈🏽 Steel Chair", + "Practical Plastic 💋 💋 Soap", + "🙌🏻 Incredible Granite Bacon", + "🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃🎃", + "Electronic Wooden Sausages 🌷", + "Tasty 🍺 Wooden Gloves", + "🏖️ Luxurious Metal Cheese", + "Awesome Rubber 😉 Gloves", + "Sleek Soft Car 💁🏻‍♂️", + "Licensed 👏👏👏 Fresh Salad", + "Ergonomic Frozen Towels 🐇", + "🖐🏻 Modern Rubber Keyboard", + "Tasty Concrete Pizza ✨✨", + "Handmade 😘 Plastic 😘 Chicken 😘", + "🏁 Luxurious Rubber Chicken 🏁", + "Practical Soft Fish 🤍", + "Ergonomic Bronze Shirt 😍", + "😸😸 Handcrafted 🐻 Plastic Bacon", + "Unbranded 🐭 Plastic Pants", + "🤘 Modern 🤘 Wooden 🤘 Sausages", + "Handmade Steel Shoes 👍", + "Rustic 🧁🧁 Steel Bike", + "Gorgeous Frozen Salad 👩‍💻", + "Handmade Bronze Chicken 😮😸", + "Sleek 🍐 Granite Bike", + " ❌ ❌ Generic Concrete Sausages", + "Incredible 🍉 Plastic Tuna", + "Bespoke Fresh Cheese 😘", + "💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡💡", + "Licensed 🍐 Wooden 🍅 Car ", + "Recycled Fresh Fish 🤡", + "📞 Incredible Fresh Shoes", + "Practical 🐻🐻 Soft Chips", + "Small 💝 Soft Chicken", + " 💝 Intelligent Fresh Mouse 💝", + "Modern Metal 🧵 Mouse", + "🦈 Tasty Granite Gloves", + "Awesome Rubber Bike 😡😡😡😡", + "🚮 Small Steel Shirt 🚮", + "Refined 🌲 Concrete Computer", + "Sleek Frozen Shirt 👨‍🦰", + "Intelligent Concrete ➗➗➗ Shoes", + "🏅Handmade Rubber Car", + "Sleek 👨🏼‍🌾 Rubber 👨🏼‍🌾 Towels", + "Unbranded Concrete Hat🎇", + "🌀🌀Incredible Plastic Fish", + "Practical Soft Gloves🌶️ 🌶️", + "Organic 🍞 Stone 🔽 Pizza 🥴", + "Generic Wooden Keyboard 💙", + "Recycled 🔴 Wooden Chips", + "Incredible Rubber Chips 🍹", + "🌵 Ergonomic Granite Shirt", + "Tasty Frozen 🦄 Keyboard", + "🍣 Gorgeous Steel 🥯 Soap", + "Luxurious Plastic Chair 🧑‍🦰", + "Elegant Frozen 🧑‍🦰 Bike", + "Recycled 🟠🟠 Steel Chair", + "⭐⭐ Modern ⭐ Bronze ⭐ Sausages", + "Elegant Wooden Cheese🤘", + "Small 🎎 Plastic 🛩️ Sausages", + "*️⃣*️⃣*️⃣*️⃣ Luxurious Frozen Shoes", + "Sleek Plastic Sausages 🚩", + "Handcrafted Fresh 💮 Sausages", + "Incredible 🤢🤢 Soft Chair", + "🇬🇪 Recycled 🇲🇺 Wooden Soap", + "Soft 🦌 Rubber Duck 🐥", + "Licensed Concrete Tuna 👎👎", + "Luxurious Granite 💝 Pants", + "Refined Rubber Keyboard 💝", + "👌🏻👌🏻 Handcrafted Plastic Computer", + "Practical Steel 🐪 Salad", + "Incredible Soft Bacon 🌺", + "Practical Metal 🥊 Fish", + "Elegant 👩🏾‍❤️‍💋‍👨🏽👩🏾‍❤️‍💋‍👨🏽 Rubber Shirt", + "🛺 Handcrafted Rubber Table", + "Gorgeous 🦙 Wooden Table 🦙", + "🍉 Fantastic Steel Sausages", + "Small Soft Keyboard👟", + "Generic 🦙🦙 Steel Ball", + "Electronic Frozen Hat ✌🏾✌🏾✌🏾✌🏾✌🏾", + "Gorgeous 🍏 Fresh Chair", + "Sleek Soft 💧 Sausages", + "Gorgeous Wooden Towels 🍿", + "Bespoke 🌅 Granite Pizza", + "Generic Metal Salad 🎗️", + "✨ Handmade ✨ Rubber ✨ Cheese ✨", + "Fantastic 🐥 🌺 🤷🏾 Steel Chair", + "Handcrafted Frozen Computer 🛡️ 🧸 🐓", + "🐹 Rustic Rubber Mouse", + "💠 Sleek Granite Pizza 💠", + "Gorgeous 🧝🏻‍♂️ Plastic Keyboard", + ], +}; + +export const defaultTodoText = { + en: "Something to do", + "zh-cn": "做某事", + ja: "何かをする必要がある", + es: "Algo que hacer", + de: "Etwas zu tun", + ru: "Кое-что сделать", + emoji: "Something to do 😊", +}; + +export const defaultLanguage = "en"; + +export function getTodoText(lang = "en", index) { + const todosSelection = todos[lang]; + const currentIndex = index % todosSelection.length; + return todosSelection[currentIndex]; +} diff --git a/experimental/javascript-wc-indexeddb/dist/src/storage/base-storage-manager.js b/experimental/javascript-wc-indexeddb/dist/src/storage/base-storage-manager.js new file mode 100644 index 000000000..fe9690a09 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/storage/base-storage-manager.js @@ -0,0 +1,68 @@ +class BaseStorageManager { + constructor() { + this.dbName = "todoDB"; + this.storeName = "todos"; + this.db = null; + this.pendingAdditions = 0; + this.pendingToggles = 0; + this.pendingDeletions = 0; + } + + async initDB() { + throw new Error("initDB method must be implemented by subclass"); + } + + _ensureDbConnection() { + if (!this.db) + throw new Error("Database connection is not established"); + } + + _handleAddComplete() { + if (--this.pendingAdditions === 0) + window.dispatchEvent(new CustomEvent("db-add-completed", {})); + } + + _handleToggleComplete() { + if (--this.pendingToggles === 0) + window.dispatchEvent(new CustomEvent("db-toggle-completed", {})); + } + + _handleRemoveComplete() { + if (--this.pendingDeletions === 0) + window.dispatchEvent(new CustomEvent("db-remove-completed", {})); + } + + _dispatchReadyEvent() { + window.dispatchEvent(new CustomEvent("db-ready", {})); + } + + _incrementPendingAdditions() { + this.pendingAdditions++; + } + + _incrementPendingToggles() { + this.pendingToggles++; + } + + _incrementPendingDeletions() { + this.pendingDeletions++; + } + + addTodo(todo) { + throw new Error("addTodo method must be implemented by subclass"); + } + + async getTodos(upperItemNumber, count) { + throw new Error("getTodos method must be implemented by subclass"); + } + + toggleTodo(itemNumber, completed) { + throw new Error("toggleTodo method must be implemented by subclass"); + } + + removeTodo(itemNumber) { + throw new Error("removeTodo method must be implemented by subclass"); + } +} + +export default BaseStorageManager; diff --git a/experimental/javascript-wc-indexeddb/dist/src/storage/dexieDB-manager.js b/experimental/javascript-wc-indexeddb/dist/src/storage/dexieDB-manager.js new file mode 100644 index 000000000..5970287e7 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/storage/dexieDB-manager.js @@ -0,0 +1,100 @@ +import Dexie from "../../libs/dexie.mjs"; +import BaseStorageManager from "./base-storage-manager.js"; + +class DexieDBManager extends BaseStorageManager { + constructor() { + super(); + this.initDB().then(() => { + this._dispatchReadyEvent(); + }); + } + + async initDB() { + // Delete the existing database first for clean state + await Dexie.delete(this.dbName); + + // Create new Dexie database + this.db = new Dexie(this.dbName); + + // Define schema + this.db.version(1).stores({ + todos: "itemNumber, id, title, completed, priority", + }); + + // Open the database + await this.db.open(); + + return this.db; + } + + addTodo(todo) { + this._ensureDbConnection(); + + this._incrementPendingAdditions(); + // Add todo item to Dexie + this.db.todos + .add(todo) + .then(() => { + // When running in Speedometer, the event will be dispatched only once + // because all the additions are done in a tight loop. + this._handleAddComplete(); + }) + .catch((error) => { + throw error; + }); + } + + async getTodos(upperItemNumber, count) { + this._ensureDbConnection(); + + // Get items with itemNumber less than upperItemNumber + // Use reverse to get highest first, then limit, then reverse result back to ascending + const items = await this.db.todos.where("itemNumber").below(upperItemNumber).reverse().limit(count).toArray(); + + // Reverse to get ascending order (lowest itemNumber first) to match IndexedDB implementation + return items.reverse(); + } + + toggleTodo(itemNumber, completed) { + this._ensureDbConnection(); + + this._incrementPendingToggles(); + + // Get the todo item and update it + this.db.todos + .get(itemNumber) + .then((todoItem) => { + if (!todoItem) + throw new Error(`Todo item with itemNumber '${itemNumber}' not found`); + + // Update the completed status + todoItem.completed = completed; + + // Save the updated item back to the database + return this.db.todos.put(todoItem); + }) + .then(() => { + this._handleToggleComplete(); + }) + .catch((error) => { + throw error; + }); + } + + removeTodo(itemNumber) { + this._ensureDbConnection(); + + this._incrementPendingDeletions(); + // Delete the todo item + this.db.todos + .delete(itemNumber) + .then(() => { + this._handleRemoveComplete(); + }) + .catch((error) => { + throw error; + }); + } +} + +export default DexieDBManager; diff --git a/experimental/javascript-wc-indexeddb/dist/src/storage/indexedDB-manager.js b/experimental/javascript-wc-indexeddb/dist/src/storage/indexedDB-manager.js new file mode 100644 index 000000000..0b84d3b23 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/storage/indexedDB-manager.js @@ -0,0 +1,183 @@ +import BaseStorageManager from "./base-storage-manager.js"; + +class IndexedDBManager extends BaseStorageManager { + constructor() { + super(); + this.dbVersion = 1; + this.initDB().then(() => { + this._dispatchReadyEvent(); + }); + } + + initDB() { + return new Promise((resolve, reject) => { + // Delete the existing database first for clean state + const deleteRequest = indexedDB.deleteDatabase(this.dbName); + + deleteRequest.onerror = (event) => { + reject(event.target.error); + }; + + deleteRequest.onsuccess = () => { + this.openDatabase(resolve, reject); + }; + + deleteRequest.onblocked = () => { + reject(new Error("Database deletion blocked - please close other tabs using this database")); + }; + }); + } + + openDatabase(resolve, reject) { + const request = indexedDB.open(this.dbName, this.dbVersion); + + request.onerror = (event) => { + reject(event.target.error); + }; + + request.onsuccess = (event) => { + this.db = event.target.result; + resolve(this.db); + }; + + request.onupgradeneeded = (event) => { + const db = event.target.result; + + // Create object store (since we're always creating a fresh DB now) + const store = db.createObjectStore(this.storeName, { keyPath: "itemNumber" }); + store.createIndex("id", "id", { unique: true }); + store.createIndex("title", "title", { unique: false }); + store.createIndex("completed", "completed", { unique: false }); + store.createIndex("priority", "priority", { unique: false }); + }; + } + + addTodo(todo) { + this._ensureDbConnection(); + + // Add todo item to IndexedDB + const transaction = this.db.transaction(this.storeName, "readwrite"); + const store = transaction.objectStore(this.storeName); + + store.add(todo); + this._incrementPendingAdditions(); + + transaction.oncomplete = () => { + // When running in Speedometer, the event will be dispatched only once + // because all the additions are done in a tight loop. + this._handleAddComplete(); + }; + + transaction.onerror = (event) => { + throw event.target.error; + }; + + transaction.commit(); + } + + async getTodos(upperItemNumber, count) { + this._ensureDbConnection(); + + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(this.storeName, "readonly"); + const store = transaction.objectStore(this.storeName); + + // Use IDBKeyRange to get items with itemNumber less than upperItemNumber + const range = IDBKeyRange.upperBound(upperItemNumber, true); // true = exclusive bound + + // Open a cursor to iterate through records in descending order + const request = store.openCursor(range, "prev"); + + const items = []; + let itemsProcessed = 0; + + request.onsuccess = (event) => { + const cursor = event.target.result; + + // Check if we have a valid cursor and haven't reached our count limit + if (cursor && itemsProcessed < count) { + items.push(cursor.value); + itemsProcessed++; + cursor.continue(); // Move to next item + } else { + // We're done - sort items by itemNumber in descending order + // for proper display order (newest to oldest) + items.sort((a, b) => a.itemNumber - b.itemNumber); + resolve(items); + } + }; + + transaction.onerror = (event) => { + reject(event.target.error); + }; + }); + } + + toggleTodo(itemNumber, completed) { + this._ensureDbConnection(); + + // Access the todo item directly by its itemNumber (keyPath) + const transaction = this.db.transaction(this.storeName, "readwrite"); + const store = transaction.objectStore(this.storeName); + + // Get the todo item directly using its primary key (itemNumber) + const getRequest = store.get(itemNumber); + + this._incrementPendingToggles(); + + getRequest.onsuccess = (event) => { + const todoItem = getRequest.result; + + if (!todoItem) + throw new Error(`Todo item with itemNumber '${itemNumber}' not found`); + + // Update the completed status + todoItem.completed = completed; + // Save the updated item back to the database + const updateRequest = store.put(todoItem); + + updateRequest.onerror = (event) => { + throw event.target.error; + }; + + transaction.commit(); + }; + + getRequest.onerror = (event) => { + throw event.target.error; + }; + + transaction.oncomplete = () => { + this._handleToggleComplete(); + }; + + // Handle transaction errors + transaction.onerror = (event) => { + throw event.target.error; + }; + } + + removeTodo(itemNumber) { + this._ensureDbConnection(); + + // Access the todo item directly by its itemNumber (keyPath) + const transaction = this.db.transaction(this.storeName, "readwrite"); + const store = transaction.objectStore(this.storeName); + + // Delete the todo item directly using its primary key (itemNumber) + store.delete(itemNumber); + this._incrementPendingDeletions(); + + transaction.oncomplete = () => { + this._handleRemoveComplete(); + }; + + transaction.onerror = (event) => { + throw event.target.error; + }; + + transaction.commit(); + } +} + +export default IndexedDBManager; diff --git a/experimental/javascript-wc-indexeddb/dist/src/storage/storage-factory.js b/experimental/javascript-wc-indexeddb/dist/src/storage/storage-factory.js new file mode 100644 index 000000000..574f42128 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/storage/storage-factory.js @@ -0,0 +1,24 @@ +import IndexedDBManager from "./indexedDB-manager.js"; +import DexieDBManager from "./dexieDB-manager.js"; + +/** + * Factory function that returns the appropriate storage manager based on URL search parameters + * @returns {IndexedDBManager|DexieDBManager} The storage manager instance + */ +export function createStorageManager() { + const params = new URLSearchParams(window.location.search); + let storageType = params.get("storageType"); + if (storageType && storageType !== "vanilla" && storageType !== "dexie") + throw new Error(`Invalid storage type specified in URL parameter: ${storageType}`); + + storageType = storageType || "vanilla"; + + if (storageType === "dexie") { + console.log("Using Dexie.js storage manager"); + return new DexieDBManager(); + } + + // Default to vanilla IndexedDB + console.log("Using vanilla IndexedDB storage manager"); + return new IndexedDBManager(); +} diff --git a/experimental/javascript-wc-indexeddb/dist/src/utils/nanoid.js b/experimental/javascript-wc-indexeddb/dist/src/utils/nanoid.js new file mode 100644 index 000000000..5df154f1f --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/utils/nanoid.js @@ -0,0 +1,41 @@ +/* Borrowed from https://github.com/ai/nanoid/blob/3.0.2/non-secure/index.js + +The MIT License (MIT) + +Copyright 2017 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + +// This alphabet uses `A-Za-z0-9_-` symbols. +// The order of characters is optimized for better gzip and brotli compression. +// References to the same file (works both for gzip and brotli): +// `'use`, `andom`, and `rict'` +// References to the brotli default dictionary: +// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf` +let urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; + +export function nanoid(size = 21) { + let id = ""; + // A compact alternative for `for (var i = 0; i < step; i++)`. + let i = size; + while (i--) { + // `| 0` is more compact and faster than `Math.floor()`. + id += urlAlphabet[(Math.random() * 64) | 0]; + } + return id; +} diff --git a/experimental/javascript-wc-indexeddb/dist/src/workload-test.mjs b/experimental/javascript-wc-indexeddb/dist/src/workload-test.mjs new file mode 100644 index 000000000..590cb9327 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/src/workload-test.mjs @@ -0,0 +1,84 @@ +import { BenchmarkStep, BenchmarkSuite } from "./speedometer-utils/benchmark.mjs"; +import { getTodoText, defaultLanguage } from "./speedometer-utils/translations.mjs"; +import { numberOfItemsToAdd } from "./speedometer-utils/todomvc-utils.mjs"; + +export const appName = "todomvc-indexeddb"; +export const appVersion = "1.0.0"; + +const suites = { + default: new BenchmarkSuite("indexeddb", [ + new BenchmarkStep(`Adding${numberOfItemsToAdd}Items`, async () => { + const input = document.querySelector("todo-app").shadowRoot.querySelector("todo-topbar").shadowRoot.querySelector(".new-todo-input"); + for (let i = 0; i < numberOfItemsToAdd; i++) { + input.value = getTodoText(defaultLanguage, i); + input.dispatchEvent(new Event("input")); + input.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter" })); + } + }), + new BenchmarkStep( + "FinishAddingItemsToDB", + async () => { + await window.addPromise; + }, + /* ignoreResult = */ true + ), + new BenchmarkStep("CompletingAllItems", async () => { + const numberOfItemsPerIteration = 10; + const numberOfIterations = 10; + window.numberOfItemsToAdd = numberOfItemsToAdd; + for (let j = 0; j < numberOfIterations; j++) { + const todoList = document.querySelector("todo-app").shadowRoot.querySelector("todo-list"); + const items = todoList.shadowRoot.querySelectorAll("todo-item"); + for (let i = 0; i < numberOfItemsPerIteration; i++) { + const item = items[i].shadowRoot.querySelector(".toggle-todo-input"); + item.click(); + } + if (j < 9) { + const nextPageButton = document.querySelector("todo-app").shadowRoot.querySelector("todo-bottombar").shadowRoot.querySelector(".next-page-button"); + nextPageButton.click(); + } + } + }), + new BenchmarkStep( + "FinishModifyingItemsInDB", + async () => { + await window.togglePromise; + }, + /* ignoreResult = */ true + ), + new BenchmarkStep("DeletingAllItems", async () => { + const numberOfItemsPerIteration = 10; + const numberOfIterations = 10; + window.numberOfItemsToAdd = numberOfItemsToAdd; + function iterationFinishedListener() { + iterationFinishedListener.promiseResolve(); + } + window.addEventListener("previous-page-loaded", iterationFinishedListener); + for (let j = 0; j < numberOfIterations; j++) { + const iterationFinishedPromise = new Promise((resolve) => { + iterationFinishedListener.promiseResolve = resolve; + }); + const todoList = document.querySelector("todo-app").shadowRoot.querySelector("todo-list"); + const items = todoList.shadowRoot.querySelectorAll("todo-item"); + for (let i = numberOfItemsPerIteration - 1; i >= 0; i--) { + const item = items[i].shadowRoot.querySelector(".remove-todo-button"); + item.click(); + } + if (j < 9) { + const previousPageButton = document.querySelector("todo-app").shadowRoot.querySelector("todo-bottombar").shadowRoot.querySelector(".previous-page-button"); + previousPageButton.click(); + await iterationFinishedPromise; + } + } + }), + new BenchmarkStep( + "FinishDeletingItemsFromDB", + async () => { + await window.removePromise; + }, + /* ignoreResult = */ true + ), + ]), +}; + +export default suites; diff --git a/experimental/javascript-wc-indexeddb/dist/styles/app.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/app.constructable.js new file mode 100644 index 000000000..8ac77f26a --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/styles/app.constructable.js @@ -0,0 +1,15 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; + min-height: 68px; +} + +.app { + background: #fff; + margin: 24px 16px 40px 16px; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} +`); +export default sheet; diff --git a/experimental/javascript-wc-indexeddb/dist/styles/bottombar.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/bottombar.constructable.js new file mode 100644 index 000000000..46904de8a --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/styles/bottombar.constructable.js @@ -0,0 +1,158 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +.bottombar { + padding: 10px 0; + height: 41px; + text-align: center; + font-size: 15px; + border-top: 1px solid #e6e6e6; + position: relative; +} + +.bottombar::before { + content: ""; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + pointer-events: none; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-status { + text-align: left; + padding: 3px; + height: 32px; + line-height: 26px; + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); +} + +.todo-count { + font-weight: 300; +} + +.filter-list { + margin: 0; + padding: 0; + list-style: none; + display: inline-block; + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +.filter-item { + display: inline-block; +} + +.filter-link { + color: inherit; + margin: 3px; + padding: 0 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; + cursor: pointer; + display: block; + height: 26px; + line-height: 26px; +} + +.filter-link:hover { + border-color: #db7676; +} + +.filter-link.selected { + border-color: #ce4646; +} + +.clear-completed-button, +.clear-completed-button:active { + text-decoration: none; + cursor: pointer; + padding: 3px; + height: 32px; + line-height: 26px; + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); +} + +.clear-completed-button:hover { + text-decoration: underline; +} + +/* rtl support */ +html[dir="rtl"] .todo-status, +:host([dir="rtl"]) .todo-status { + right: 12px; + left: unset; +} + +html[dir="rtl"] .clear-completed-button, +:host([dir="rtl"]) .clear-completed-button { + left: 12px; + right: unset; +} + +@media (max-width: 430px) { + .bottombar { + height: 120px; + } + + .todo-status { + display: block; + text-align: center; + position: relative; + left: unset; + right: unset; + top: unset; + transform: unset; + } + + .filter-list { + display: block; + position: relative; + left: unset; + right: unset; + top: unset; + transform: unset; + } + + .clear-completed-button, + .clear-completed-button:active { + display: block; + margin: 0 auto; + position: relative; + left: unset; + right: unset; + top: unset; + transform: unset; + } + + html[dir="rtl"] .todo-status, + :host([dir="rtl"]) .todo-status { + right: unset; + left: unset; + } + + html[dir="rtl"] .clear-completed-button, + :host([dir="rtl"]) .clear-completed-button { + left: unset; + right: unset; + } +} +`); +export default sheet; diff --git a/experimental/javascript-wc-indexeddb/dist/styles/footer.css b/experimental/javascript-wc-indexeddb/dist/styles/footer.css new file mode 100644 index 000000000..0ff918f43 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/styles/footer.css @@ -0,0 +1,26 @@ +:host { + display: block; + box-shadow: none !important; +} + +.footer { + margin: 65px auto 0; + color: #4d4d4d; + font-size: 11px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.footer-text { + line-height: 1; +} + +.footer-link { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.footer-link:hover { + text-decoration: underline; +} diff --git a/experimental/javascript-wc-indexeddb/dist/styles/global.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/global.constructable.js new file mode 100644 index 000000000..7ff85b07f --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/styles/global.constructable.js @@ -0,0 +1,86 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +body { + font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #111; + min-width: 300px; + max-width: 582px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + box-shadow: inset 0 0 2px 2px #cf7d7d !important; + outline: 0 !important; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +input { + position: relative; + margin: 0; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + color: inherit; + padding: 0; + border: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +input:placeholder-shown { + text-overflow: ellipsis; +} + + +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + position: absolute; + white-space: nowrap; +} + +.truncate-singleline { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block !important; +} +`); +export default sheet; diff --git a/experimental/javascript-wc-indexeddb/dist/styles/global.css b/experimental/javascript-wc-indexeddb/dist/styles/global.css new file mode 100644 index 000000000..22ee5e16c --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/styles/global.css @@ -0,0 +1,88 @@ +/** defaults */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +body { + font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #111; + min-width: 300px; + max-width: 582px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + box-shadow: inset 0 0 2px 2px #cf7d7d !important; + outline: 0 !important; +} + +/** resets */ +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +input { + position: relative; + margin: 0; + font-size: inherit; + font-family: inherit; + font-weight: inherit; + color: inherit; + padding: 0; + border: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +input:placeholder-shown { + text-overflow: ellipsis; +} + +/* utility classes */ + +/* used for things that should be hidden in the ui, +but useful for people who use screen readers */ +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + position: absolute; + white-space: nowrap; +} + +.truncate-singleline { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block !important; +} diff --git a/experimental/javascript-wc-indexeddb/dist/styles/header.css b/experimental/javascript-wc-indexeddb/dist/styles/header.css new file mode 100644 index 000000000..56d2a4064 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/styles/header.css @@ -0,0 +1,21 @@ +:host { + display: block; + box-shadow: none !important; +} + +.header { + margin-top: 27px; +} + +.title { + width: 100%; + font-size: 80px; + line-height: 80px; + margin: 0; + font-weight: 200; + text-align: center; + color: #b83f45; + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} diff --git a/experimental/javascript-wc-indexeddb/dist/styles/main.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/main.constructable.js new file mode 100644 index 000000000..66ea0b30c --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/styles/main.constructable.js @@ -0,0 +1,11 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +.main { + position: relative; +} +`); +export default sheet; diff --git a/experimental/javascript-wc-indexeddb/dist/styles/todo-item.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/todo-item.constructable.js new file mode 100644 index 000000000..59dba7f77 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/styles/todo-item.constructable.js @@ -0,0 +1,147 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +:host(:last-child) > .todo-item { + border-bottom: none; +} + +.todo-item { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; + height: 60px; +} + +.todo-item.editing { + border-bottom: none; + padding: 0; +} + +.edit-todo-container { + display: none; +} + +.todo-item.editing .edit-todo-container { + display: block; +} + +.edit-todo-input { + padding: 0 16px 0 60px; + width: 100%; + height: 60px; + font-size: 24px; + line-height: 1.4em; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20%20style%3D%22opacity%3A%200.2%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; +} + +.display-todo { + position: relative; +} + +.todo-item.editing .display-todo { + display: none; +} + +.toggle-todo-input { + text-align: center; + width: 40px; + + height: auto; + position: absolute; + top: 0; + bottom: 0; + left: 3px; + margin: auto 0; + border: none; appearance: none; + cursor: pointer; +} + +.todo-item-text { + overflow-wrap: break-word; + padding: 0 60px; + display: block; + line-height: 60px; + transition: color 0.4s; + font-weight: 400; + color: #484848; + + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; +} + +.toggle-todo-input:checked + .todo-item-text { + color: #949494; + text-decoration: line-through; + background-image: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E"); +} + +.remove-todo-button { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #949494; + transition: color 0.2s ease-out; + cursor: pointer; +} + +.remove-todo-button:hover, +.remove-todo-button:focus { + color: #c18585; +} + +.remove-todo-button::after { + content: "×"; + display: block; + height: 100%; + line-height: 1.1; +} + +.todo-item:hover .remove-todo-button { + display: block; +} + +@media screen and (-webkit-min-device-pixel-ratio: 0) { + .toggle-todo-input { + background: none; + height: 40px; + } +} + +@media (max-width: 430px) { + .remove-todo-button { + display: block; + } +} + +html[dir="rtl"] .toggle-todo-input, +:host([dir="rtl"]) .toggle-todo-input { + right: 3px; + left: unset; +} + +html[dir="rtl"] .todo-item-text, +:host([dir="rtl"]) .todo-item-text { + background-position: center right 6px; +} + +html[dir="rtl"] .remove-todo-button, +:host([dir="rtl"]) .remove-todo-button { + left: 10px; + right: unset; +} +`); +export default sheet; diff --git a/experimental/javascript-wc-indexeddb/dist/styles/todo-list.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/todo-list.constructable.js new file mode 100644 index 000000000..3d11133dd --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/styles/todo-list.constructable.js @@ -0,0 +1,15 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; + display: block; + border-top: 1px solid #e6e6e6; +} +`); +export default sheet; diff --git a/experimental/javascript-wc-indexeddb/dist/styles/topbar.constructable.js b/experimental/javascript-wc-indexeddb/dist/styles/topbar.constructable.js new file mode 100644 index 000000000..5ecb8a231 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/dist/styles/topbar.constructable.js @@ -0,0 +1,90 @@ +const sheet = new CSSStyleSheet(); +sheet.replaceSync(`:host { + display: block; + box-shadow: none !important; +} + +.topbar { + position: relative; +} + +.new-todo-input { + padding: 0 32px 0 60px; + width: 100%; + height: 68px; + font-size: 24px; + line-height: 1.4em; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); +} + +.new-todo-input::placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.toggle-all-container { + width: 45px; + height: 68px; + position: absolute; + left: 0; + top: 0; +} + +.toggle-all-input { + width: 45px; + height: 45px; + font-size: 0; + position: absolute; + top: 11.5px; + left: 0; + border: none; + appearance: none; + cursor: pointer; +} + +.toggle-all-label { + display: flex; + align-items: center; + justify-content: center; + width: 45px; + height: 68px; + font-size: 0; + position: absolute; + top: 0; + left: 0; + cursor: pointer; +} + +.toggle-all-label::before { + content: "❯"; + display: inline-block; + font-size: 22px; + color: #949494; + padding: 10px 27px 10px 27px; + transform: rotate(90deg); +} + +.toggle-all-input:checked + .toggle-all-label::before { + color: #484848; +} + +@media screen and (-webkit-min-device-pixel-ratio: 0) { + .toggle-all-input { + background: none; + } +} + +html[dir="rtl"] .new-todo-input, +:host([dir="rtl"]) .new-todo-input { + padding: 0 60px 0 32px; +} + +html[dir="rtl"] .toggle-all-container, +:host([dir="rtl"]) .toggle-all-container { + right: 0; + left: unset; +} +`); +export default sheet; diff --git a/experimental/javascript-wc-indexeddb/index.html b/experimental/javascript-wc-indexeddb/index.html new file mode 100644 index 000000000..8e0d54267 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/index.html @@ -0,0 +1,31 @@ + + + + + + + TodoMVC: JavaScript Web Components + + + + + + + + + + + +
    +

    todos

    +
    + +
    + + + + +
    + + + diff --git a/experimental/javascript-wc-indexeddb/package-lock.json b/experimental/javascript-wc-indexeddb/package-lock.json new file mode 100644 index 000000000..7c296bf51 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/package-lock.json @@ -0,0 +1,813 @@ +{ + "name": "todomvc-javascript-web-components-indexeddb", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "todomvc-javascript-web-components-indexeddb", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "dexie": "^4.2.1", + "http-server": "^14.1.1", + "speedometer-utils": "../../resources/shared", + "todomvc-css": "file:../../resources/todomvc/todomvc-css" + }, + "engines": { + "node": ">=18.13.0", + "npm": ">=8.19.3" + } + }, + "../../../resources/shared": { + "extraneous": true + }, + "../../../shared": { + "extraneous": true + }, + "../../resources/shared": { + "name": "speedometer-utils", + "version": "1.0.0" + }, + "../../resources/todomvc/todomvc-css": { + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@rollup/plugin-babel": "^6.0.3", + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-html": "^1.0.2", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-terser": "^0.4.3", + "@rollup/pluginutils": "^5.0.2", + "fs-extra": "^11.1.1", + "globby": "^13.2.0", + "http-server": "^14.1.1", + "rollup": "^3.23.0", + "rollup-plugin-cleaner": "^1.0.0", + "rollup-plugin-copy-merge": "^1.0.2", + "rollup-plugin-import-css": "^3.2.1", + "strip-comments": "^2.0.1", + "stylelint": "^15.6.2", + "stylelint-config-standard": "^33.0.0" + }, + "engines": { + "node": ">=18.13.0", + "npm": ">=8.19.3" + } + }, + "../../todomvc-css": { + "version": "1.0.0", + "extraneous": true, + "license": "ISC", + "devDependencies": { + "@rollup/plugin-babel": "^6.0.3", + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-html": "^1.0.2", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-terser": "^0.4.3", + "@rollup/pluginutils": "^5.0.2", + "fs-extra": "^11.1.1", + "globby": "^13.2.0", + "http-server": "^14.1.1", + "rollup": "^3.23.0", + "rollup-plugin-cleaner": "^1.0.0", + "rollup-plugin-copy-merge": "^1.0.2", + "rollup-plugin-import-css": "^3.2.1", + "strip-comments": "^2.0.1", + "stylelint": "^15.6.2", + "stylelint-config-standard": "^33.0.0" + }, + "engines": { + "node": ">=18.13.0", + "npm": ">=8.19.3" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/dexie": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.2.1.tgz", + "integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==", + "license": "Apache-2.0" + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/speedometer-utils": { + "resolved": "../../resources/shared", + "link": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/todomvc-css": { + "resolved": "../../resources/todomvc/todomvc-css", + "link": true + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + } + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "requires": { + "lodash": "^4.17.14" + } + }, + "basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==" + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "dexie": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.2.1.tgz", + "integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==" + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, + "html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "requires": { + "whatwg-encoding": "^2.0.0" + } + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "requires": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==" + }, + "portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "requires": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + } + }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "speedometer-utils": { + "version": "file:../../resources/shared" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "todomvc-css": { + "version": "file:../../resources/todomvc/todomvc-css", + "requires": { + "@rollup/plugin-babel": "^6.0.3", + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-html": "^1.0.2", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-terser": "^0.4.3", + "@rollup/pluginutils": "^5.0.2", + "fs-extra": "^11.1.1", + "globby": "^13.2.0", + "http-server": "^14.1.1", + "rollup": "^3.23.0", + "rollup-plugin-cleaner": "^1.0.0", + "rollup-plugin-copy-merge": "^1.0.2", + "rollup-plugin-import-css": "^3.2.1", + "strip-comments": "^2.0.1", + "stylelint": "^15.6.2", + "stylelint-config-standard": "^33.0.0" + } + }, + "union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "requires": { + "qs": "^6.4.0" + } + }, + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, + "whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "requires": { + "iconv-lite": "0.6.3" + } + } + } +} diff --git a/experimental/javascript-wc-indexeddb/package.json b/experimental/javascript-wc-indexeddb/package.json new file mode 100644 index 000000000..c8d06962c --- /dev/null +++ b/experimental/javascript-wc-indexeddb/package.json @@ -0,0 +1,24 @@ +{ + "name": "todomvc-javascript-web-components-indexeddb", + "version": "1.0.0", + "description": "TodoMVC app written with JavaScript using web components.", + "engines": { + "node": ">=18.13.0", + "npm": ">=8.19.3" + }, + "private": true, + "scripts": { + "dev": "http-server ./ -p 7005 -c-1 --cors -o", + "build": "node scripts/build.js", + "serve": "http-server ./dist -p 7006 -c-1 --cors -o" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "dexie": "^4.2.1", + "http-server": "^14.1.1", + "todomvc-css": "file:../../resources/todomvc/todomvc-css", + "speedometer-utils": "../../resources/shared" + } +} diff --git a/experimental/javascript-wc-indexeddb/scripts/build.js b/experimental/javascript-wc-indexeddb/scripts/build.js new file mode 100644 index 000000000..2ae9ec0c5 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/scripts/build.js @@ -0,0 +1,162 @@ +const fs = require("fs").promises; +const { dirname } = require("path"); + +/** + * createDirectory + * + * Removes and recreates a directory. + * + * @param {string} directory Directory name. + */ +async function createDirectory(directory) { + await fs.rm(directory, { recursive: true, force: true }); + await fs.mkdir(directory); +} + +/** + * copyDirectory + * + * Copies a source folder to a destination folder. + * + * @param {string} src Source directory. + * @param {string} dest Destination directory. + */ +async function copyDirectory(src, dest) { + await fs.cp(src, dest, { recursive: true }, (err) => { + if (err) + console.error(err); + }); +} + +/** + * copyFile + * + * Copies a file from a source to a destination. + * + * @param {string} src Source file. + * @param {string} dest Destination file. + */ +async function copyFile(src, dest) { + await fs.mkdir(dirname(dest), { recursive: true }); + await fs.copyFile(src, dest); +} + +/** + * copyFiles + * + * Copies multiple files from a source to a destination. + * + * @param {string[]} files Array of files to copy. + */ +async function copyFiles(files) { + for (const file of files) + await copyFile(file.src, file.dest); +} + +/** + * updateImportsInFile + * + * Reads a file and replaces a source path with a destination path. + * + * @param {Object} config - Config to update imports + * @param {string} config.src - The source path. + * @param {string} config.dest - The destination path. + * @param {string} config.file - File to read from. + */ +async function updateImportsInFile({ file, src, dest }) { + let contents = await fs.readFile(file, "utf8"); + contents = contents.replaceAll(src, dest); + await fs.writeFile(file, contents); +} + +/** + * updateImports + * + * Updates imports in multiple files. + * + * @param {Object} config - Config to update imports + * @param {string} config.src - The source path. + * @param {string} config.dest - The destination path. + * @param {string} config.file - Files to read from. + */ +async function updateImports({ files, src, dest }) { + for (const file of files) + await updateImportsInFile({ file, src, dest }); +} + +const filesToMove = [ + { src: "index.html", dest: "./dist/index.html" }, + { src: "node_modules/todomvc-css/dist/global.css", dest: "./dist/styles/global.css" }, + { src: "node_modules/todomvc-css/dist/header.css", dest: "./dist/styles/header.css" }, + { src: "node_modules/todomvc-css/dist/footer.css", dest: "./dist/styles/footer.css" }, + { src: "node_modules/todomvc-css/dist/global.constructable.js", dest: "./dist/styles/global.constructable.js" }, + { src: "node_modules/todomvc-css/dist/app.constructable.js", dest: "./dist/styles/app.constructable.js" }, + { src: "node_modules/todomvc-css/dist/topbar.constructable.js", dest: "./dist/styles/topbar.constructable.js" }, + { src: "node_modules/todomvc-css/dist/main.constructable.js", dest: "./dist/styles/main.constructable.js" }, + { src: "node_modules/todomvc-css/dist/bottombar.constructable.js", dest: "./dist/styles/bottombar.constructable.js" }, + { src: "node_modules/todomvc-css/dist/todo-list.constructable.js", dest: "./dist/styles/todo-list.constructable.js" }, + { src: "node_modules/todomvc-css/dist/todo-item.constructable.js", dest: "./dist/styles/todo-item.constructable.js" }, + { src: "node_modules/dexie/dist/modern/dexie.mjs", dest: "./dist/libs/dexie.mjs" }, + { src: "src/speedometer-utils/test-invoker.mjs", dest: "./dist/src/speedometer-utils/test-invoker.mjs" }, + { src: "src/speedometer-utils/test-runner.mjs", dest: "./dist/src/speedometer-utils/test-runner.mjs" }, + { src: "src/speedometer-utils/params.mjs", dest: "./dist/src/speedometer-utils/params.mjs" }, + { src: "src/speedometer-utils/benchmark.mjs", dest: "./dist/src/speedometer-utils/benchmark.mjs" }, + { src: "src/speedometer-utils/helpers.mjs", dest: "./dist/src/speedometer-utils/helpers.mjs" }, + { src: "node_modules/speedometer-utils/translations.mjs", dest: "./dist/src/speedometer-utils/translations.mjs" }, + { src: "node_modules/speedometer-utils/todomvc-utils.mjs", dest: "./dist/src/speedometer-utils/todomvc-utils.mjs" }, +]; + +const importsToRename = [ + { + src: "node_modules/todomvc-css/dist/", + dest: "styles/", + files: ["./dist/index.html"], + }, + { + src: "../../../node_modules/todomvc-css/dist/", + dest: "../../../styles/", + files: [ + "./dist/src/components/todo-app/todo-app.component.js", + "./dist/src/components/todo-bottombar/todo-bottombar.component.js", + "./dist/src/components/todo-item/todo-item.component.js", + "./dist/src/components/todo-list/todo-list.component.js", + "./dist/src/components/todo-topbar/todo-topbar.component.js", + ], + }, + { + src: "../../../node_modules/dexie/dist/modern/dexie.mjs", + dest: "../../libs/dexie.mjs", + files: ["./dist/src/storage/dexieDB-manager.js"], + }, + { + src: "/src/", + dest: "./", + files: ["./dist/src/index.mjs"], + }, + { + src: "/node_modules/speedometer-utils/", + dest: "./speedometer-utils/", + files: ["./dist/src/index.mjs", "./dist/src/workload-test.mjs"], + }, +]; + +const build = async () => { + // create dist folder + await createDirectory("./dist"); + + // copy src folder + await copyDirectory("./src", "./dist/src"); + + // copy files to Move + await copyFiles(filesToMove); + + // rename imports files + for (const entry of importsToRename) { + const { files, src, dest } = entry; + await updateImports({ files, src, dest }); + } + + console.log("Done with building!"); +}; + +build(); diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js b/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js new file mode 100644 index 000000000..e4e03bdfb --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.component.js @@ -0,0 +1,156 @@ +import template from "./todo-app.template.js"; +import { useRouter } from "../../hooks/useRouter.js"; + +import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; +import appStyles from "../../../node_modules/todomvc-css/dist/app.constructable.js"; +import mainStyles from "../../../node_modules/todomvc-css/dist/main.constructable.js"; +class TodoApp extends HTMLElement { + #isReady = false; + #numberOfItems = 0; + #numberOfCompletedItems = 0; + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.topbar = node.querySelector("todo-topbar"); + this.list = node.querySelector("todo-list"); + this.bottombar = node.querySelector("todo-bottombar"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, appStyles, mainStyles]; + this.shadow.append(node); + + this.addItem = this.addItem.bind(this); + this.toggleItem = this.toggleItem.bind(this); + this.removeItem = this.removeItem.bind(this); + this.updateItem = this.updateItem.bind(this); + this.toggleItems = this.toggleItems.bind(this); + this.clearCompletedItems = this.clearCompletedItems.bind(this); + this.routeChange = this.routeChange.bind(this); + this.moveToNextPage = this.moveToNextPage.bind(this); + this.moveToPreviousPage = this.moveToPreviousPage.bind(this); + + this.router = useRouter(); + } + + get isReady() { + return this.#isReady; + } + + getInstance() { + return this; + } + + addItem(event) { + const { detail: item } = event; + this.list.addItem(item, this.#numberOfItems++); + this.update(); + } + + toggleItem(event) { + if (event.detail.completed) + this.#numberOfCompletedItems++; + else + this.#numberOfCompletedItems--; + + this.list.toggleItem(event.detail.itemNumber, event.detail.completed); + this.update(); + } + + removeItem(event) { + if (event.detail.completed) + this.#numberOfCompletedItems--; + + this.#numberOfItems--; + this.update(); + this.list.removeItem(event.detail.itemNumber); + } + + updateItem(event) { + this.update(); + } + + toggleItems(event) { + this.list.toggleItems(event.detail.completed); + } + + clearCompletedItems() { + this.list.removeCompletedItems(); + } + + moveToNextPage() { + this.list.moveToNextPage(); + } + + async moveToPreviousPage() { + await this.list.moveToPreviousPage(); + this.bottombar.reenablePreviousPageButton(); + window.dispatchEvent(new CustomEvent("previous-page-loaded", {})); + } + + update() { + const totalItems = this.#numberOfItems; + const completedItems = this.#numberOfCompletedItems; + const activeItems = totalItems - completedItems; + + this.list.setAttribute("total-items", totalItems); + + this.topbar.setAttribute("total-items", totalItems); + this.topbar.setAttribute("active-items", activeItems); + this.topbar.setAttribute("completed-items", completedItems); + + this.bottombar.setAttribute("total-items", totalItems); + this.bottombar.setAttribute("active-items", activeItems); + } + + addListeners() { + this.topbar.addEventListener("toggle-all", this.toggleItems); + this.topbar.addEventListener("add-item", this.addItem); + + this.list.listNode.addEventListener("toggle-item", this.toggleItem); + this.list.listNode.addEventListener("remove-item", this.removeItem); + this.list.listNode.addEventListener("update-item", this.updateItem); + + this.bottombar.addEventListener("clear-completed-items", this.clearCompletedItems); + this.bottombar.addEventListener("next-page", this.moveToNextPage); + this.bottombar.addEventListener("previous-page", this.moveToPreviousPage); + } + + removeListeners() { + this.topbar.removeEventListener("toggle-all", this.toggleItems); + this.topbar.removeEventListener("add-item", this.addItem); + + this.list.listNode.removeEventListener("toggle-item", this.toggleItem); + this.list.listNode.removeEventListener("remove-item", this.removeItem); + this.list.listNode.removeEventListener("update-item", this.updateItem); + + this.bottombar.removeEventListener("clear-completed-items", this.clearCompletedItems); + this.bottombar.removeEventListener("next-page", this.moveToNextPage); + this.bottombar.removeEventListener("previous-page", this.moveToPreviousPage); + } + + routeChange(route) { + const routeName = route.split("/")[1] || "all"; + this.list.updateRoute(routeName); + this.bottombar.updateRoute(routeName); + this.topbar.updateRoute(routeName); + } + + connectedCallback() { + this.update(); + this.addListeners(); + this.router.initRouter(this.routeChange); + this.#isReady = true; + } + + disconnectedCallback() { + this.removeListeners(); + this.#isReady = false; + } +} + +customElements.define("todo-app", TodoApp); + +export default TodoApp; diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.template.js b/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.template.js new file mode 100644 index 000000000..1a55a8194 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/components/todo-app/todo-app.template.js @@ -0,0 +1,14 @@ +const template = document.createElement("template"); + +template.id = "todo-app-template"; +template.innerHTML = ` +
    + +
    + +
    + +
    +`; + +export default template; diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js b/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js new file mode 100644 index 000000000..0b9d2f19a --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.component.js @@ -0,0 +1,126 @@ +import template from "./todo-bottombar.template.js"; + +import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; +import bottombarStyles from "../../../node_modules/todomvc-css/dist/bottombar.constructable.js"; + +const customStyles = new CSSStyleSheet(); +customStyles.replaceSync(` + + .clear-completed-button, .clear-completed-button:active, + .todo-status, + .filter-list + { + position: unset; + transform: unset; + } + + .bottombar { + display: grid; + grid-template-columns: repeat(7, 1fr); + align-items: center; + justify-items: center; + } + + .bottombar > * { + grid-column: span 1; + } + + .filter-list { + grid-column: span 3; + } + + :host([total-items="0"]) > .bottombar { + display: none; + } +`); + +class TodoBottombar extends HTMLElement { + static get observedAttributes() { + return ["total-items", "active-items"]; + } + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.element = node.querySelector(".bottombar"); + this.clearCompletedButton = node.querySelector(".clear-completed-button"); + this.todoStatus = node.querySelector(".todo-status"); + this.filterLinks = node.querySelectorAll(".filter-link"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, bottombarStyles, customStyles]; + this.shadow.append(node); + + this.clearCompletedItems = this.clearCompletedItems.bind(this); + this.MoveToNextPage = this.MoveToNextPage.bind(this); + this.MoveToPreviousPage = this.MoveToPreviousPage.bind(this); + } + + updateDisplay() { + this.todoStatus.textContent = `${this["active-items"]} ${this["active-items"] === "1" ? "item" : "items"} left!`; + } + + updateRoute(route) { + this.filterLinks.forEach((link) => { + if (link.dataset.route === route) + link.classList.add("selected"); + else + link.classList.remove("selected"); + }); + } + + clearCompletedItems() { + this.dispatchEvent(new CustomEvent("clear-completed-items")); + } + + MoveToNextPage() { + this.dispatchEvent(new CustomEvent("next-page")); + } + + MoveToPreviousPage() { + this.element.querySelector(".previous-page-button").disabled = true; + this.dispatchEvent(new CustomEvent("previous-page")); + } + + addListeners() { + this.clearCompletedButton.addEventListener("click", this.clearCompletedItems); + this.element.querySelector(".next-page-button").addEventListener("click", this.MoveToNextPage); + this.element.querySelector(".previous-page-button").addEventListener("click", this.MoveToPreviousPage); + } + + removeListeners() { + this.clearCompletedButton.removeEventListener("click", this.clearCompletedItems); + this.getElementById("next-page-button").removeEventListener("click", this.MoveToNextPage); + this.getElementById("previous-page-button").removeEventListener("click", this.MoveToPreviousPage); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.updateDisplay(); + } + + reenablePreviousPageButton() { + this.element.querySelector(".previous-page-button").disabled = false; + window.dispatchEvent(new CustomEvent("previous-page-button-enabled", {})); + } + + connectedCallback() { + this.updateDisplay(); + this.addListeners(); + } + + disconnectedCallback() { + this.removeListeners(); + } +} + +customElements.define("todo-bottombar", TodoBottombar); + +export default TodoBottombar; diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js b/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js new file mode 100644 index 000000000..e9259fe30 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/components/todo-bottombar/todo-bottombar.template.js @@ -0,0 +1,24 @@ +const template = document.createElement("template"); + +template.id = "todo-bottombar-template"; +template.innerHTML = ` +
    +
    0 item left
    + + + + +
    +`; + +export default template; diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.component.js b/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.component.js new file mode 100644 index 000000000..6def10994 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.component.js @@ -0,0 +1,182 @@ +import template from "./todo-item.template.js"; +import { useDoubleClick } from "../../hooks/useDoubleClick.js"; +import { useKeyListener } from "../../hooks/useKeyListener.js"; + +import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; +import itemStyles from "../../../node_modules/todomvc-css/dist/todo-item.constructable.js"; + +class TodoItem extends HTMLElement { + static get observedAttributes() { + return ["itemid", "itemtitle", "itemcompleted"]; + } + + constructor() { + super(); + + // Renamed this.id to this.itemid and this.title to this.itemtitle. + // When the component assigns to this.id or this.title, this causes the browser's implementation of the existing setters to run, which convert these property sets into internal setAttribute calls. This can have surprising consequences. + // [Issue]: https://github.com/WebKit/Speedometer/issues/313 + this.itemid = ""; + this.itemtitle = "Todo Item"; + this.itemcompleted = "false"; + this.itemIndex = null; + + const node = document.importNode(template.content, true); + this.item = node.querySelector(".todo-item"); + this.toggleLabel = node.querySelector(".toggle-todo-label"); + this.toggleInput = node.querySelector(".toggle-todo-input"); + this.todoText = node.querySelector(".todo-item-text"); + this.todoButton = node.querySelector(".remove-todo-button"); + this.editLabel = node.querySelector(".edit-todo-label"); + this.editInput = node.querySelector(".edit-todo-input"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, itemStyles]; + this.shadow.append(node); + + this.keysListeners = []; + + this.updateItem = this.updateItem.bind(this); + this.toggleItem = this.toggleItem.bind(this); + this.removeItem = this.removeItem.bind(this); + this.startEdit = this.startEdit.bind(this); + this.stopEdit = this.stopEdit.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + + if (window.extraTodoItemCssToAdopt) { + let extraAdoptedStyleSheet = new CSSStyleSheet(); + extraAdoptedStyleSheet.replaceSync(window.extraTodoItemCssToAdopt); + this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet); + } + } + + update(...args) { + args.forEach((argument) => { + switch (argument) { + case "itemid": + if (this.itemid !== undefined) + this.item.id = `todo-item-${this.itemid}`; + break; + case "itemtitle": + if (this.itemtitle !== undefined) { + this.todoText.textContent = this.itemtitle; + this.editInput.value = this.itemtitle; + } + break; + case "itemcompleted": + this.toggleInput.checked = this.itemcompleted === "true"; + break; + } + }); + } + + startEdit() { + this.item.classList.add("editing"); + this.editInput.value = this.itemtitle; + this.editInput.focus(); + } + + stopEdit() { + this.item.classList.remove("editing"); + } + + cancelEdit() { + this.editInput.blur(); + } + + toggleItem() { + // The todo-list checks the "completed" attribute to filter based on route + // (therefore the completed state needs to already be updated before the check) + this.setAttribute("itemcompleted", this.toggleInput.checked); + + this.dispatchEvent( + new CustomEvent("toggle-item", { + detail: { completed: this.toggleInput.checked, itemNumber: this.itemIndex }, + bubbles: true, + }) + ); + } + + removeItem() { + this.dispatchEvent( + new CustomEvent("remove-item", { + detail: { completed: this.toggleInput.checked, itemNumber: this.itemIndex }, + bubbles: true, + }) + ); + this.remove(); + } + + updateItem(event) { + if (event.target.value !== this.itemtitle) { + if (!event.target.value.length) + this.removeItem(); + else + this.setAttribute("itemtitle", event.target.value); + } + + this.cancelEdit(); + } + + addListeners() { + this.toggleInput.addEventListener("change", this.toggleItem); + this.todoText.addEventListener("click", useDoubleClick(this.startEdit, 500)); + this.editInput.addEventListener("blur", this.stopEdit); + this.todoButton.addEventListener("click", this.removeItem); + + this.keysListeners.forEach((listener) => listener.connect()); + } + + removeListeners() { + this.toggleInput.removeEventListener("change", this.toggleItem); + this.todoText.removeEventListener("click", this.startEdit); + this.editInput.removeEventListener("blur", this.stopEdit); + this.todoButton.removeEventListener("click", this.removeItem); + + this.keysListeners.forEach((listener) => listener.disconnect()); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.update(property); + } + + connectedCallback() { + this.update("itemid", "itemtitle", "itemcompleted"); + + this.keysListeners.push( + useKeyListener({ + target: this.editInput, + event: "keyup", + callbacks: { + ["Enter"]: this.updateItem, + ["Escape"]: this.cancelEdit, + }, + }), + useKeyListener({ + target: this.todoText, + event: "keyup", + callbacks: { + [" "]: this.startEdit, // this feels weird + }, + }) + ); + + this.addListeners(); + } + + disconnectedCallback() { + this.removeListeners(); + this.keysListeners = []; + } +} + +customElements.define("todo-item", TodoItem); + +export default TodoItem; diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.template.js b/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.template.js new file mode 100644 index 000000000..9a67675fd --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/components/todo-item/todo-item.template.js @@ -0,0 +1,19 @@ +const template = document.createElement("template"); + +template.id = "todo-item-template"; +template.innerHTML = ` +
  • +
    + + + Placeholder Text + +
    +
    + + +
    +
  • +`; + +export default template; diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js b/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js new file mode 100644 index 000000000..e5bf29205 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.component.js @@ -0,0 +1,175 @@ +import template from "./todo-list.template.js"; +import TodoItem from "../todo-item/todo-item.component.js"; +import { createStorageManager } from "../../storage/storage-factory.js"; + +import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; +import listStyles from "../../../node_modules/todomvc-css/dist/todo-list.constructable.js"; + +const MAX_ON_SCREEN_ITEMS = 10; + +const customListStyles = new CSSStyleSheet(); +customListStyles.replaceSync(` + .todo-list[route="completed"] > [itemcompleted="false"] { + display: none; + } + + .todo-list[route="active"] > [itemcompleted="true"] { + display: none; + } + + :nth-child(${MAX_ON_SCREEN_ITEMS}) ~ todo-item { + display: none; + } +`); + +class TodoList extends HTMLElement { + static get observedAttributes() { + return ["total-items"]; + } + + #route = undefined; + #firstItemIndexOnScreen = 0; + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.listNode = node.querySelector(".todo-list"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, listStyles, customListStyles]; + this.shadow.append(node); + this.classList.add("show-priority"); + this.storageManager = createStorageManager(); + + if (window.extraTodoListCssToAdopt) { + let extraAdoptedStyleSheet = new CSSStyleSheet(); + extraAdoptedStyleSheet.replaceSync(window.extraTodoListCssToAdopt); + this.shadow.adoptedStyleSheets.push(extraAdoptedStyleSheet); + } + } + + addItem(entry, itemIndex) { + const { id, title, completed } = entry; + const priority = 4 - (itemIndex % 5); + const element = new TodoItem(); + + element.setAttribute("itemid", id); + element.setAttribute("itemtitle", title); + element.setAttribute("itemcompleted", completed); + element.setAttribute("data-priority", priority); + element.itemIndex = itemIndex; + + this.listNode.append(element); + + this.#addItemToStorage(itemIndex, id, title, priority, completed); + } + + removeItem(itemIndex) { + this.storageManager.removeTodo(itemIndex); + } + + addItems(items) { + items.forEach((entry) => this.addItem(entry)); + } + + removeCompletedItems() { + Array.from(this.listNode.children).forEach((element) => { + if (element.itemcompleted === "true") + element.removeItem(); + }); + } + + toggleItems(completed) { + Array.from(this.listNode.children).forEach((element) => { + if (completed && element.itemcompleted === "false") + element.toggleInput.click(); + else if (!completed && element.itemcompleted === "true") + element.toggleInput.click(); + }); + } + + toggleItem(itemNumber, completed) { + // Update the item in the IndexedDB + this.storageManager.toggleTodo(itemNumber, completed); + } + + updateStyles() { + if (parseInt(this["total-items"]) !== 0) + this.listNode.style.display = "block"; + else + this.listNode.style.display = "none"; + } + + updateRoute(route) { + this.#route = route; + switch (route) { + case "completed": + this.listNode.setAttribute("route", "completed"); + break; + case "active": + this.listNode.setAttribute("route", "active"); + break; + default: + this.listNode.setAttribute("route", "all"); + } + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + if (this.isConnected) + this.updateStyles(); + } + + connectedCallback() { + this.updateStyles(); + } + + moveToNextPage() { + for (let i = 0; i < MAX_ON_SCREEN_ITEMS; i++) { + const child = this.listNode.firstChild; + if (!child) + break; + child.remove(); + } + this.#firstItemIndexOnScreen = this.listNode.firstChild.itemIndex; + } + + async moveToPreviousPage() { + const items = await this.storageManager.getTodos(this.#firstItemIndexOnScreen, MAX_ON_SCREEN_ITEMS); + const elements = items.map((item) => { + const { id, title, completed, priority } = item; + const element = new TodoItem(); + element.setAttribute("itemid", id); + element.setAttribute("itemtitle", title); + element.setAttribute("itemcompleted", completed); + element.setAttribute("data-priority", priority); + element.itemIndex = item.itemNumber; + return element; + }); + this.#firstItemIndexOnScreen = items[0].itemNumber; + this.listNode.replaceChildren(...elements); + } + + #addItemToStorage(itemIndex, id, title, priority, completed) { + // Create a todo object with the structure expected by IndexedDB + const todoItem = { + itemNumber: itemIndex, + id, + title, + completed, + priority, + }; + + // Add the item to IndexedDB and handle the Promise + this.storageManager.addTodo(todoItem); + } +} + +customElements.define("todo-list", TodoList); + +export default TodoList; diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.template.js b/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.template.js new file mode 100644 index 000000000..e92320b51 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/components/todo-list/todo-list.template.js @@ -0,0 +1,8 @@ +const template = document.createElement("template"); + +template.id = "todo-list-template"; +template.innerHTML = ` +
      +`; + +export default template; diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.component.js b/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.component.js new file mode 100644 index 000000000..4d65bd177 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.component.js @@ -0,0 +1,138 @@ +import template from "./todo-topbar.template.js"; +import { useKeyListener } from "../../hooks/useKeyListener.js"; +import { nanoid } from "../../utils/nanoid.js"; + +import globalStyles from "../../../node_modules/todomvc-css/dist/global.constructable.js"; +import topbarStyles from "../../../node_modules/todomvc-css/dist/topbar.constructable.js"; + +const customListStyles = new CSSStyleSheet(); +customListStyles.replaceSync(` + :host([total-items="0"]) .toggle-all-container { + display: none; + } +`); + +class TodoTopbar extends HTMLElement { + static get observedAttributes() { + return ["active-items", "completed-items"]; + } + + #route = undefined; + + constructor() { + super(); + + const node = document.importNode(template.content, true); + this.todoInput = node.querySelector("#new-todo"); + this.toggleInput = node.querySelector("#toggle-all"); + this.toggleContainer = node.querySelector(".toggle-all-container"); + + this.shadow = this.attachShadow({ mode: "open" }); + this.htmlDirection = document.dir || "ltr"; + this.setAttribute("dir", this.htmlDirection); + this.shadow.adoptedStyleSheets = [globalStyles, topbarStyles, customListStyles]; + this.shadow.append(node); + + this.keysListeners = []; + + this.toggleAll = this.toggleAll.bind(this); + this.addItem = this.addItem.bind(this); + } + + toggleAll(event) { + this.dispatchEvent( + new CustomEvent("toggle-all", { + detail: { completed: event.target.checked }, + }) + ); + } + + addItem(event) { + if (!event.target.value.length) + return; + + this.dispatchEvent( + new CustomEvent("add-item", { + detail: { + id: nanoid(), + title: event.target.value, + completed: false, + }, + }) + ); + + event.target.value = ""; + } + + updateDisplay() { + if (!parseInt(this["total-items"])) { + this.toggleContainer.style.display = "none"; + return; + } + + this.toggleContainer.style.display = "block"; + + switch (this.#route) { + case "active": + this.toggleInput.checked = false; + this.toggleInput.disabled = !parseInt(this["active-items"]); + break; + case "completed": + this.toggleInput.checked = parseInt(this["completed-items"]); + this.toggleInput.disabled = !parseInt(this["completed-items"]); + break; + default: + this.toggleInput.checked = !parseInt(this["active-items"]); + this.toggleInput.disabled = false; + } + } + + updateRoute(route) { + this.#route = route; + this.updateDisplay(); + } + + addListeners() { + this.toggleInput.addEventListener("change", this.toggleAll); + this.keysListeners.forEach((listener) => listener.connect()); + } + + removeListeners() { + this.toggleInput.removeEventListener("change", this.toggleAll); + this.keysListeners.forEach((listener) => listener.disconnect()); + } + + attributeChangedCallback(property, oldValue, newValue) { + if (oldValue === newValue) + return; + this[property] = newValue; + + if (this.isConnected) + this.updateDisplay(); + } + + connectedCallback() { + this.keysListeners.push( + useKeyListener({ + target: this.todoInput, + event: "keyup", + callbacks: { + ["Enter"]: this.addItem, + }, + }) + ); + + this.updateDisplay(); + this.addListeners(); + this.todoInput.focus(); + } + + disconnectedCallback() { + this.removeListeners(); + this.keysListeners = []; + } +} + +customElements.define("todo-topbar", TodoTopbar); + +export default TodoTopbar; diff --git a/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.template.js b/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.template.js new file mode 100644 index 000000000..e7e5286a3 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/components/todo-topbar/todo-topbar.template.js @@ -0,0 +1,17 @@ +const template = document.createElement("template"); + +template.id = "todo-topbar-template"; +template.innerHTML = ` +
      +
      + + +
      +
      + + +
      +
      +`; + +export default template; diff --git a/experimental/javascript-wc-indexeddb/src/hooks/useDoubleClick.js b/experimental/javascript-wc-indexeddb/src/hooks/useDoubleClick.js new file mode 100644 index 000000000..a1fe952fe --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/hooks/useDoubleClick.js @@ -0,0 +1,19 @@ +/** + * A simple function to normalize a double-click and a double-tab action. + * There is currently no comparable tab action to dblclick. + * + * @param {Function} fn + * @param {number} delay + * @returns + */ +export function useDoubleClick(fn, delay) { + let last = 0; + return function (...args) { + const now = new Date().getTime(); + const difference = now - last; + if (difference < delay && difference > 0) + fn.apply(this, args); + + last = now; + }; +} diff --git a/experimental/javascript-wc-indexeddb/src/hooks/useKeyListener.js b/experimental/javascript-wc-indexeddb/src/hooks/useKeyListener.js new file mode 100644 index 000000000..453747d54 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/hooks/useKeyListener.js @@ -0,0 +1,23 @@ +export function useKeyListener(props) { + const { target, event, callbacks } = props; + + function handleEvent(event) { + Object.keys(callbacks).forEach((key) => { + if (event.key === key) + callbacks[key](event); + }); + } + + function connect() { + target.addEventListener(event, handleEvent); + } + + function disconnect() { + target.removeEventListener(event, handleEvent); + } + + return { + connect, + disconnect, + }; +} diff --git a/experimental/javascript-wc-indexeddb/src/hooks/useRouter.js b/experimental/javascript-wc-indexeddb/src/hooks/useRouter.js new file mode 100644 index 000000000..ab1ab618a --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/hooks/useRouter.js @@ -0,0 +1,43 @@ +/** + * Listens for hash change of the url and calls onChange if available. + * + * @param {Function} callback + * @returns Methods to interact with useRouter. + */ +export const useRouter = (callback) => { + let onChange = callback; + let current = ""; + + /** + * Change event handler. + */ + const handleChange = () => { + current = document.location.hash; + /* istanbul ignore else */ + if (onChange) + onChange(document.location.hash); + }; + + /** + * Initializes router and adds listeners. + * + * @param {Function} callback + */ + const initRouter = (callback) => { + onChange = callback; + window.addEventListener("hashchange", handleChange); + window.addEventListener("load", handleChange); + }; + + /** + * Removes listeners + */ + const disableRouter = () => { + window.removeEventListener("hashchange", handleChange); + window.removeEventListener("load", handleChange); + }; + + const getRoute = () => current.split("/").slice(-1)[0]; + + return { initRouter, getRoute, disableRouter }; +}; diff --git a/experimental/javascript-wc-indexeddb/src/index.mjs b/experimental/javascript-wc-indexeddb/src/index.mjs new file mode 100644 index 000000000..17fe6baa9 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/index.mjs @@ -0,0 +1,23 @@ +import { BenchmarkConnector } from "/node_modules/speedometer-utils/benchmark.mjs"; +import suites, { appName, appVersion } from "/src/workload-test.mjs"; + +window.workloadPromises = {}; +window.workloadPromises.addPromise = new Promise((resolve) => { + window.addEventListener("db-add-completed", () => resolve()); +}); +window.workloadPromises.togglePromise = new Promise((resolve) => { + window.addEventListener("db-toggle-completed", () => resolve()); +}); +window.workloadPromises.deletePromise = new Promise((resolve) => { + window.addEventListener("db-delete-completed", () => resolve()); +}); + +window.addEventListener("db-ready", () => { + /* + Paste below into dev console for manual testing: + window.addEventListener("message", (event) => console.log(event.data)); + window.postMessage({ id: "todomvc-postmessage-1.0.0", key: "benchmark-connector", type: "benchmark-suite", name: "default" }, "*"); + */ + const benchmarkConnector = new BenchmarkConnector(suites, appName, appVersion); + benchmarkConnector.connect(); +}); diff --git a/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs b/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs new file mode 100644 index 000000000..8d842fb9e --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/speedometer-utils/benchmark.mjs @@ -0,0 +1,131 @@ +/* eslint-disable no-case-declarations */ +import { TestRunner } from "./test-runner.mjs"; +import { Params } from "./params.mjs"; + +/** + * BenchmarkStep + * + * A single test step, with a common interface to interact with. + */ +export class BenchmarkStep { + constructor(name, run, ignoreResult = false) { + this.name = name; + this.run = run; + this.ignoreResult = ignoreResult; + } + + async runAndRecord(params, suite, test, callback) { + const testRunner = new TestRunner(null, null, params, suite, test, callback); + const result = await testRunner.runTest(); + return result; + } +} + +/** + * BenchmarkSuite + * + * A single test suite that contains one or more test steps. + */ +export class BenchmarkSuite { + constructor(name, tests) { + this.name = name; + this.tests = tests; + } + + record(_test, syncTime, asyncTime) { + const total = syncTime + asyncTime; + const results = { + tests: { Sync: syncTime, Async: asyncTime }, + total: total, + }; + + return results; + } + + async runAndRecord(params, onProgress) { + const measuredValues = { + tests: {}, + total: 0, + }; + const suiteStartLabel = `suite-${this.name}-start`; + const suiteEndLabel = `suite-${this.name}-end`; + + performance.mark(suiteStartLabel); + + for (const test of this.tests) { + const result = await test.runAndRecord(params, this, test, this.record); + if (!test.ignoreResult) { + measuredValues.tests[test.name] = result; + measuredValues.total += result.total; + } + onProgress?.(test.name); + } + + performance.mark(suiteEndLabel); + performance.measure(`suite-${this.name}`, suiteStartLabel, suiteEndLabel); + + return { + type: "suite-tests-complete", + status: "success", + result: measuredValues, + suitename: this.name, + }; + } +} + +/** ********************************************************************** + * BenchmarkConnector + * + * postMessage is used to communicate between app and benchmark. + * When the app is ready, an 'app-ready' message is sent to signal that the app can receive instructions. + * + * A prepare script within the apps appends window.name and window.version from the package.json file. + * The appId is build by appending name-version + * It's used as an additional safe-guard to ensure the correct app responds to a message. + *************************************************************************/ +export class BenchmarkConnector { + constructor(suites, name, version) { + this.suites = suites; + this.name = name; + this.version = version; + + if (!name || !version) + console.warn("No name or version supplied, to create a unique appId"); + + this.appId = name && version ? `${name}-${version}` : -1; + this.onMessage = this.onMessage.bind(this); + } + + async onMessage(event) { + if (event.data.id !== this.appId || event.data.key !== "benchmark-connector") + return; + + switch (event.data.type) { + case "benchmark-suite": + const params = new Params(new URLSearchParams(window.location.search)); + const suite = this.suites[event.data.name]; + if (!suite) + console.error(`Suite with the name of "${event.data.name}" not found!`); + const { result } = await suite.runAndRecord(params, (test) => this.sendMessage({ type: "step-complete", status: "success", appId: this.appId, name: this.name, test })); + console.log(result, result.tests); + this.sendMessage({ type: "suite-complete", status: "success", appId: this.appId, result }); + this.disconnect(); + break; + default: + console.error(`Message data type not supported: ${event.data.type}`); + } + } + + sendMessage(message) { + window.top.postMessage(message, "*"); + } + + connect() { + window.addEventListener("message", this.onMessage); + this.sendMessage({ type: "app-ready", status: "success", appId: this.appId }); + } + + disconnect() { + window.removeEventListener("message", this.onMessage); + } +} diff --git a/experimental/javascript-wc-indexeddb/src/speedometer-utils/helpers.mjs b/experimental/javascript-wc-indexeddb/src/speedometer-utils/helpers.mjs new file mode 100644 index 000000000..729d5f3a4 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/speedometer-utils/helpers.mjs @@ -0,0 +1,30 @@ +/** + * Helper Methods + * + * Various methods that are extracted from the Page class. + */ +export function getParent(lookupStartNode, path) { + lookupStartNode = lookupStartNode.shadowRoot ?? lookupStartNode; + const parent = path.reduce((root, selector) => { + const node = root.querySelector(selector); + return node.shadowRoot ?? node; + }, lookupStartNode); + + return parent; +} + +export function getElement(selector, path = [], lookupStartNode = document) { + const element = getParent(lookupStartNode, path).querySelector(selector); + return element; +} + +export function getAllElements(selector, path = [], lookupStartNode = document) { + const elements = Array.from(getParent(lookupStartNode, path).querySelectorAll(selector)); + return elements; +} + +export function forceLayout() { + const rect = document.body.getBoundingClientRect(); + const e = document.elementFromPoint((rect.width / 2) | 0, (rect.height / 2) | 0); + return e; +} diff --git a/experimental/javascript-wc-indexeddb/src/speedometer-utils/params.mjs b/experimental/javascript-wc-indexeddb/src/speedometer-utils/params.mjs new file mode 100644 index 000000000..701222d97 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/speedometer-utils/params.mjs @@ -0,0 +1,195 @@ +export class Params { + viewport = { + width: 800, + height: 600, + }; + // Enable a detailed developer menu to change the current Params. + developerMode = false; + startAutomatically = false; + iterationCount = 10; + suites = []; + // A list of tags to filter suites + tags = []; + // Toggle running a dummy suite once before the normal test suites. + useWarmupSuite = false; + // toggle async type vs default raf type. + useAsyncSteps = false; + // Change how a test measurement is triggered and async time is measured: + // "timer": The classic (as in Speedometer 2.x) way using setTimeout + // "raf": Using rAF callbacks, both for triggering the sync part and for measuring async time. + measurementMethod = "raf"; + // Wait time before the sync step in ms. + waitBeforeSync = 0; + // Warmup time before the sync step in ms. + warmupBeforeSync = 0; + // Seed for shuffling the execution order of suites. + // "off": do not shuffle + // "generate": generate a random seed + // : use the provided integer as a seed + shuffleSeed = "off"; + + constructor(searchParams = undefined) { + if (searchParams) + this._copyFromSearchParams(searchParams); + if (!this.developerMode) { + Object.freeze(this.viewport); + Object.freeze(this); + } + } + + _parseInt(value, errorMessage) { + const number = Number(value); + if (!Number.isInteger(number) && errorMessage) + throw new Error(`Invalid ${errorMessage} param: '${value}', expected int.`); + return parseInt(number); + } + + _copyFromSearchParams(searchParams) { + this.viewport = this._parseViewport(searchParams); + this.startAutomatically = this._parseBooleanParam(searchParams, "startAutomatically"); + this.iterationCount = this._parseIntParam(searchParams, "iterationCount", 1); + this.suites = this._parseSuites(searchParams); + this.tags = this._parseTags(searchParams); + this.developerMode = this._parseBooleanParam(searchParams, "developerMode"); + this.useWarmupSuite = this._parseBooleanParam(searchParams, "useWarmupSuite"); + this.useAsyncSteps = this._parseBooleanParam(searchParams, "useAsyncSteps"); + this.waitBeforeSync = this._parseIntParam(searchParams, "waitBeforeSync", 0); + this.warmupBeforeSync = this._parseIntParam(searchParams, "warmupBeforeSync", 0); + this.measurementMethod = this._parseMeasurementMethod(searchParams); + this.shuffleSeed = this._parseShuffleSeed(searchParams); + + const unused = Array.from(searchParams.keys()); + if (unused.length > 0) + console.error("Got unused search params", unused); + } + + _parseBooleanParam(searchParams, paramKey) { + if (!searchParams.has(paramKey)) + return false; + searchParams.delete(paramKey); + return true; + } + + _parseIntParam(searchParams, paramKey, minValue) { + if (!searchParams.has(paramKey)) + return defaultParams[paramKey]; + + const parsedValue = this._parseInt(searchParams.get(paramKey), "waitBeforeSync"); + if (parsedValue < minValue) + throw new Error(`Invalid ${paramKey} param: '${parsedValue}', value must be >= ${minValue}.`); + searchParams.delete(paramKey); + return parsedValue; + } + + _parseViewport(searchParams) { + if (!searchParams.has("viewport")) + return defaultParams.viewport; + const viewportParam = searchParams.get("viewport"); + const [width, height] = viewportParam.split("x"); + const viewport = { + width: this._parseInt(width, "viewport.width"), + height: this._parseInt(height, "viewport.height"), + }; + if (this.viewport.width < 800 || this.viewport.height < 600) + throw new Error(`Invalid viewport param: ${viewportParam}`); + searchParams.delete("viewport"); + return viewport; + } + + _parseSuites(searchParams) { + if (searchParams.has("suite") || searchParams.has("suites")) { + if (searchParams.has("suite") && searchParams.has("suites")) + throw new Error("Params 'suite' and 'suites' can not be used together."); + const value = searchParams.get("suite") || searchParams.get("suites"); + const suites = value.split(","); + if (suites.length === 0) + throw new Error("No suites selected"); + searchParams.delete("suite"); + searchParams.delete("suites"); + return suites; + } + return defaultParams.suites; + } + + _parseTags(searchParams) { + if (!searchParams.has("tags")) + return defaultParams.tags; + if (this.suites.length) + throw new Error("'suites' and 'tags' cannot be used together."); + const tags = searchParams.get("tags").split(","); + searchParams.delete("tags"); + return tags; + } + + _parseMeasurementMethod(searchParams) { + if (!searchParams.has("measurementMethod")) + return defaultParams.measurementMethod; + const measurementMethod = searchParams.get("measurementMethod"); + if (measurementMethod !== "raf") + throw new Error(`Invalid measurement method: '${measurementMethod}', must be 'raf'.`); + searchParams.delete("measurementMethod"); + return measurementMethod; + } + + _parseShuffleSeed(searchParams) { + if (!searchParams.has("shuffleSeed")) + return defaultParams.shuffleSeed; + let shuffleSeed = searchParams.get("shuffleSeed"); + if (shuffleSeed !== "off") { + if (shuffleSeed === "generate") { + shuffleSeed = Math.floor((Math.random() * 1) << 16); + console.log(`Generated a random suite order seed: ${shuffleSeed}`); + } else { + shuffleSeed = parseInt(shuffleSeed); + } + if (!Number.isInteger(shuffleSeed)) + throw new Error(`Invalid shuffle seed: '${shuffleSeed}', must be either 'off', 'generate' or an integer.`); + } + searchParams.delete("shuffleSeed"); + return shuffleSeed; + } + + toCompleteSearchParamsObject() { + return this.toSearchParamsObject(false); + } + + toSearchParamsObject(filter = true) { + const rawUrlParams = { __proto__: null }; + for (const [key, value] of Object.entries(this)) { + // Handle composite values separately. + if (key === "viewport" || key === "suites" || key === "tags") + continue; + // Skip over default values. + if (filter && value === defaultParams[key]) + continue; + rawUrlParams[key] = value; + } + + if (this.viewport.width !== defaultParams.viewport.width || this.viewport.height !== defaultParams.viewport.height) + rawUrlParams.viewport = `${this.viewport.width}x${this.viewport.height}`; + + if (this.suites.length) + rawUrlParams.suites = this.suites.join(","); + else if (this.tags.length) + rawUrlParams.tags = this.tags.join(","); + + return new URLSearchParams(rawUrlParams); + } + + toSearchParams() { + return this.toSearchParamsObject().toString(); + } +} + +export const defaultParams = new Params(); + +let maybeCustomParams = defaultParams; +if (globalThis?.location?.search) { + const searchParams = new URLSearchParams(globalThis.location.search); + try { + maybeCustomParams = new Params(searchParams); + } catch (e) { + console.error("Invalid URL Param", e, "\nUsing defaults as fallback:", maybeCustomParams); + } +} +export const params = maybeCustomParams; diff --git a/experimental/javascript-wc-indexeddb/src/speedometer-utils/test-invoker.mjs b/experimental/javascript-wc-indexeddb/src/speedometer-utils/test-invoker.mjs new file mode 100644 index 000000000..e83a95ba8 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/speedometer-utils/test-invoker.mjs @@ -0,0 +1,86 @@ +class TestInvoker { + constructor(syncCallback, asyncCallback, reportCallback, params) { + this._syncCallback = syncCallback; + this._asyncCallback = asyncCallback; + this._reportCallback = reportCallback; + this._params = params; + } +} + +class BaseRAFTestInvoker extends TestInvoker { + start() { + return new Promise((resolve) => { + if (this._params.waitBeforeSync) + setTimeout(() => this._scheduleCallbacks(resolve), this._params.waitBeforeSync); + else + this._scheduleCallbacks(resolve); + }); + } +} + +class RAFTestInvoker extends BaseRAFTestInvoker { + _scheduleCallbacks(resolve) { + requestAnimationFrame(() => this._syncCallback()); + requestAnimationFrame(() => { + setTimeout(() => { + this._asyncCallback(); + setTimeout(async () => { + const result = await this._reportCallback(); + resolve(result); + }, 0); + }, 0); + }); + } +} + +class AsyncRAFTestInvoker extends BaseRAFTestInvoker { + static mc = new MessageChannel(); + _scheduleCallbacks(resolve) { + let gotTimer = false; + let gotMessage = false; + let gotPromise = false; + + const tryTriggerAsyncCallback = () => { + if (!gotTimer || !gotMessage || !gotPromise) + return; + + this._asyncCallback(); + setTimeout(async () => { + const results = await this._reportCallback(); + resolve(results); + }, 0); + }; + + requestAnimationFrame(async () => { + await this._syncCallback(); + gotPromise = true; + tryTriggerAsyncCallback(); + }); + + requestAnimationFrame(() => { + setTimeout(async () => { + await Promise.resolve(); + gotTimer = true; + tryTriggerAsyncCallback(); + }); + + AsyncRAFTestInvoker.mc.port1.addEventListener( + "message", + async function () { + await Promise.resolve(); + gotMessage = true; + tryTriggerAsyncCallback(); + }, + { once: true } + ); + AsyncRAFTestInvoker.mc.port1.start(); + AsyncRAFTestInvoker.mc.port2.postMessage("speedometer"); + }); + } +} + +export const TEST_INVOKER_LOOKUP = { + __proto__: null, + raf: RAFTestInvoker, + async: AsyncRAFTestInvoker, +}; diff --git a/experimental/javascript-wc-indexeddb/src/speedometer-utils/test-runner.mjs b/experimental/javascript-wc-indexeddb/src/speedometer-utils/test-runner.mjs new file mode 100644 index 000000000..52defcb36 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/speedometer-utils/test-runner.mjs @@ -0,0 +1,112 @@ +import { TEST_INVOKER_LOOKUP } from "./test-invoker.mjs"; + +export class TestRunner { + #frame; + #page; + #params; + #suite; + #test; + #callback; + #type; + + constructor(frame, page, params, suite, test, callback, type) { + this.#suite = suite; + this.#test = test; + this.#params = params; + this.#callback = callback; + this.#page = page; + this.#frame = frame; + this.#type = type; + } + + get page() { + return this.#page; + } + + get test() { + return this.#test; + } + + _runSyncStep(test, page) { + test.run(page); + } + + async runTest() { + // Prepare all mark labels outside the measuring loop. + const suiteName = this.#suite.name; + const testName = this.#test.name; + const syncStartLabel = `${suiteName}.${testName}-start`; + const syncEndLabel = `${suiteName}.${testName}-sync-end`; + const asyncEndLabel = `${suiteName}.${testName}-async-end`; + + let syncTime; + let asyncStartTime; + let asyncTime; + + const runSync = async () => { + if (this.#params.warmupBeforeSync) { + performance.mark("warmup-start"); + const startTime = performance.now(); + // Infinite loop for the specified ms. + while (performance.now() - startTime < this.#params.warmupBeforeSync) + continue; + performance.mark("warmup-end"); + } + performance.mark(syncStartLabel); + const syncStartTime = performance.now(); + + if (this.#type === "async") + await this._runSyncStep(this.test, this.page); + else + this._runSyncStep(this.test, this.page); + + const mark = performance.mark(syncEndLabel); + const syncEndTime = mark.startTime; + + syncTime = syncEndTime - syncStartTime; + asyncStartTime = syncEndTime; + }; + const measureAsync = () => { + const bodyReference = this.#frame ? this.#frame.contentDocument.body : document.body; + const windowReference = this.#frame ? this.#frame.contentWindow : window; + // Some browsers don't immediately update the layout for paint. + // Force the layout here to ensure we're measuring the layout time. + const height = bodyReference.getBoundingClientRect().height; + windowReference._unusedHeightValue = height; // Prevent dead code elimination. + + const asyncEndTime = performance.now(); + performance.mark(asyncEndLabel); + + asyncTime = asyncEndTime - asyncStartTime; + + if (this.#params.warmupBeforeSync) + performance.measure("warmup", "warmup-start", "warmup-end"); + performance.measure(`${suiteName}.${testName}-sync`, syncStartLabel, syncEndLabel); + performance.measure(`${suiteName}.${testName}-async`, syncEndLabel, asyncEndLabel); + }; + + const report = () => this.#callback(this.#test, syncTime, asyncTime); + const invokerType = this.#suite.type === "async" || this.#params.useAsyncSteps ? "async" : this.#params.measurementMethod; + const invokerClass = TEST_INVOKER_LOOKUP[invokerType]; + const invoker = new invokerClass(runSync, measureAsync, report, this.#params); + + return invoker.start(); + } +} + +export class AsyncTestRunner extends TestRunner { + constructor(frame, page, params, suite, test, callback, type) { + super(frame, page, params, suite, test, callback, type); + } + + async _runSyncStep(test, page) { + await test.run(page); + } +} + +export const TEST_RUNNER_LOOKUP = { + __proto__: null, + default: TestRunner, + async: AsyncTestRunner, + remote: TestRunner, +}; diff --git a/experimental/javascript-wc-indexeddb/src/storage/base-storage-manager.js b/experimental/javascript-wc-indexeddb/src/storage/base-storage-manager.js new file mode 100644 index 000000000..450099cb2 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/storage/base-storage-manager.js @@ -0,0 +1,129 @@ +/** + * Base class for storage managers that provides common functionality + * for tracking pending operations and dispatching events. + */ +class BaseStorageManager { + constructor() { + this.dbName = "todoDB"; + this.storeName = "todos"; + this.db = null; + this.pendingAdditions = 0; + this.pendingToggles = 0; + this.pendingDeletions = 0; + } + + /** + * Initialize the database. This method should be implemented by subclasses. + * @returns {Promise} Promise that resolves when the database is initialized + */ + async initDB() { + throw new Error("initDB method must be implemented by subclass"); + } + + /** + * Check if the database connection is established + * @throws {Error} If database connection is not established + */ + _ensureDbConnection() { + if (!this.db) + throw new Error("Database connection is not established"); + } + + /** + * Handle completion of add operations + * @protected + */ + _handleAddComplete() { + if (--this.pendingAdditions === 0) + window.dispatchEvent(new CustomEvent("db-add-completed", {})); + } + + /** + * Handle completion of toggle operations + * @protected + */ + _handleToggleComplete() { + if (--this.pendingToggles === 0) + window.dispatchEvent(new CustomEvent("db-toggle-completed", {})); + } + + /** + * Handle completion of remove operations + * @protected + */ + _handleRemoveComplete() { + if (--this.pendingDeletions === 0) + window.dispatchEvent(new CustomEvent("db-remove-completed", {})); + } + + /** + * Dispatch the db-ready event when initialization is complete + * @protected + */ + _dispatchReadyEvent() { + window.dispatchEvent(new CustomEvent("db-ready", {})); + } + + /** + * Increment the pending additions counter + * @protected + */ + _incrementPendingAdditions() { + this.pendingAdditions++; + } + + /** + * Increment the pending toggles counter + * @protected + */ + _incrementPendingToggles() { + this.pendingToggles++; + } + + /** + * Increment the pending deletions counter + * @protected + */ + _incrementPendingDeletions() { + this.pendingDeletions++; + } + + // Abstract methods that must be implemented by subclasses + + /** + * Add a todo item to the database + * @param {Object} todo - The todo item to add + */ + addTodo(todo) { + throw new Error("addTodo method must be implemented by subclass"); + } + + /** + * Get todos from the database + * @param {number} upperItemNumber - Upper bound for item numbers (exclusive) + * @param {number} count - Maximum number of items to retrieve + * @returns {Promise} Promise that resolves to an array of todo items + */ + async getTodos(upperItemNumber, count) { + throw new Error("getTodos method must be implemented by subclass"); + } + + /** + * Toggle the completed status of a todo item + * @param {number} itemNumber - The item number to toggle + * @param {boolean} completed - The new completed status + */ + toggleTodo(itemNumber, completed) { + throw new Error("toggleTodo method must be implemented by subclass"); + } + + /** + * Remove a todo item from the database + * @param {number} itemNumber - The item number to remove + */ + removeTodo(itemNumber) { + throw new Error("removeTodo method must be implemented by subclass"); + } +} + +export default BaseStorageManager; diff --git a/experimental/javascript-wc-indexeddb/src/storage/dexieDB-manager.js b/experimental/javascript-wc-indexeddb/src/storage/dexieDB-manager.js new file mode 100644 index 000000000..2e99c3596 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/storage/dexieDB-manager.js @@ -0,0 +1,100 @@ +import Dexie from "../../../node_modules/dexie/dist/modern/dexie.mjs"; +import BaseStorageManager from "./base-storage-manager.js"; + +class DexieDBManager extends BaseStorageManager { + constructor() { + super(); + this.initDB().then(() => { + this._dispatchReadyEvent(); + }); + } + + async initDB() { + // Delete the existing database first for clean state + await Dexie.delete(this.dbName); + + // Create new Dexie database + this.db = new Dexie(this.dbName); + + // Define schema + this.db.version(1).stores({ + todos: "itemNumber, id, title, completed, priority", + }); + + // Open the database + await this.db.open(); + + return this.db; + } + + addTodo(todo) { + this._ensureDbConnection(); + + this._incrementPendingAdditions(); + // Add todo item to Dexie + this.db.todos + .add(todo) + .then(() => { + // When running in Speedometer, the event will be dispatched only once + // because all the additions are done in a tight loop. + this._handleAddComplete(); + }) + .catch((error) => { + throw error; + }); + } + + async getTodos(upperItemNumber, count) { + this._ensureDbConnection(); + + // Get items with itemNumber less than upperItemNumber + // Use reverse to get highest first, then limit, then reverse result back to ascending + const items = await this.db.todos.where("itemNumber").below(upperItemNumber).reverse().limit(count).toArray(); + + // Reverse to get ascending order (lowest itemNumber first) to match IndexedDB implementation + return items.reverse(); + } + + toggleTodo(itemNumber, completed) { + this._ensureDbConnection(); + + this._incrementPendingToggles(); + + // Get the todo item and update it + this.db.todos + .get(itemNumber) + .then((todoItem) => { + if (!todoItem) + throw new Error(`Todo item with itemNumber '${itemNumber}' not found`); + + // Update the completed status + todoItem.completed = completed; + + // Save the updated item back to the database + return this.db.todos.put(todoItem); + }) + .then(() => { + this._handleToggleComplete(); + }) + .catch((error) => { + throw error; + }); + } + + removeTodo(itemNumber) { + this._ensureDbConnection(); + + this._incrementPendingDeletions(); + // Delete the todo item + this.db.todos + .delete(itemNumber) + .then(() => { + this._handleRemoveComplete(); + }) + .catch((error) => { + throw error; + }); + } +} + +export default DexieDBManager; diff --git a/experimental/javascript-wc-indexeddb/src/storage/indexedDB-manager.js b/experimental/javascript-wc-indexeddb/src/storage/indexedDB-manager.js new file mode 100644 index 000000000..0b84d3b23 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/storage/indexedDB-manager.js @@ -0,0 +1,183 @@ +import BaseStorageManager from "./base-storage-manager.js"; + +class IndexedDBManager extends BaseStorageManager { + constructor() { + super(); + this.dbVersion = 1; + this.initDB().then(() => { + this._dispatchReadyEvent(); + }); + } + + initDB() { + return new Promise((resolve, reject) => { + // Delete the existing database first for clean state + const deleteRequest = indexedDB.deleteDatabase(this.dbName); + + deleteRequest.onerror = (event) => { + reject(event.target.error); + }; + + deleteRequest.onsuccess = () => { + this.openDatabase(resolve, reject); + }; + + deleteRequest.onblocked = () => { + reject(new Error("Database deletion blocked - please close other tabs using this database")); + }; + }); + } + + openDatabase(resolve, reject) { + const request = indexedDB.open(this.dbName, this.dbVersion); + + request.onerror = (event) => { + reject(event.target.error); + }; + + request.onsuccess = (event) => { + this.db = event.target.result; + resolve(this.db); + }; + + request.onupgradeneeded = (event) => { + const db = event.target.result; + + // Create object store (since we're always creating a fresh DB now) + const store = db.createObjectStore(this.storeName, { keyPath: "itemNumber" }); + store.createIndex("id", "id", { unique: true }); + store.createIndex("title", "title", { unique: false }); + store.createIndex("completed", "completed", { unique: false }); + store.createIndex("priority", "priority", { unique: false }); + }; + } + + addTodo(todo) { + this._ensureDbConnection(); + + // Add todo item to IndexedDB + const transaction = this.db.transaction(this.storeName, "readwrite"); + const store = transaction.objectStore(this.storeName); + + store.add(todo); + this._incrementPendingAdditions(); + + transaction.oncomplete = () => { + // When running in Speedometer, the event will be dispatched only once + // because all the additions are done in a tight loop. + this._handleAddComplete(); + }; + + transaction.onerror = (event) => { + throw event.target.error; + }; + + transaction.commit(); + } + + async getTodos(upperItemNumber, count) { + this._ensureDbConnection(); + + return new Promise((resolve, reject) => { + const transaction = this.db.transaction(this.storeName, "readonly"); + const store = transaction.objectStore(this.storeName); + + // Use IDBKeyRange to get items with itemNumber less than upperItemNumber + const range = IDBKeyRange.upperBound(upperItemNumber, true); // true = exclusive bound + + // Open a cursor to iterate through records in descending order + const request = store.openCursor(range, "prev"); + + const items = []; + let itemsProcessed = 0; + + request.onsuccess = (event) => { + const cursor = event.target.result; + + // Check if we have a valid cursor and haven't reached our count limit + if (cursor && itemsProcessed < count) { + items.push(cursor.value); + itemsProcessed++; + cursor.continue(); // Move to next item + } else { + // We're done - sort items by itemNumber in descending order + // for proper display order (newest to oldest) + items.sort((a, b) => a.itemNumber - b.itemNumber); + resolve(items); + } + }; + + transaction.onerror = (event) => { + reject(event.target.error); + }; + }); + } + + toggleTodo(itemNumber, completed) { + this._ensureDbConnection(); + + // Access the todo item directly by its itemNumber (keyPath) + const transaction = this.db.transaction(this.storeName, "readwrite"); + const store = transaction.objectStore(this.storeName); + + // Get the todo item directly using its primary key (itemNumber) + const getRequest = store.get(itemNumber); + + this._incrementPendingToggles(); + + getRequest.onsuccess = (event) => { + const todoItem = getRequest.result; + + if (!todoItem) + throw new Error(`Todo item with itemNumber '${itemNumber}' not found`); + + // Update the completed status + todoItem.completed = completed; + // Save the updated item back to the database + const updateRequest = store.put(todoItem); + + updateRequest.onerror = (event) => { + throw event.target.error; + }; + + transaction.commit(); + }; + + getRequest.onerror = (event) => { + throw event.target.error; + }; + + transaction.oncomplete = () => { + this._handleToggleComplete(); + }; + + // Handle transaction errors + transaction.onerror = (event) => { + throw event.target.error; + }; + } + + removeTodo(itemNumber) { + this._ensureDbConnection(); + + // Access the todo item directly by its itemNumber (keyPath) + const transaction = this.db.transaction(this.storeName, "readwrite"); + const store = transaction.objectStore(this.storeName); + + // Delete the todo item directly using its primary key (itemNumber) + store.delete(itemNumber); + this._incrementPendingDeletions(); + + transaction.oncomplete = () => { + this._handleRemoveComplete(); + }; + + transaction.onerror = (event) => { + throw event.target.error; + }; + + transaction.commit(); + } +} + +export default IndexedDBManager; diff --git a/experimental/javascript-wc-indexeddb/src/storage/storage-factory.js b/experimental/javascript-wc-indexeddb/src/storage/storage-factory.js new file mode 100644 index 000000000..574f42128 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/storage/storage-factory.js @@ -0,0 +1,24 @@ +import IndexedDBManager from "./indexedDB-manager.js"; +import DexieDBManager from "./dexieDB-manager.js"; + +/** + * Factory function that returns the appropriate storage manager based on URL search parameters + * @returns {IndexedDBManager|DexieDBManager} The storage manager instance + */ +export function createStorageManager() { + const params = new URLSearchParams(window.location.search); + let storageType = params.get("storageType"); + if (storageType && storageType !== "vanilla" && storageType !== "dexie") + throw new Error(`Invalid storage type specified in URL parameter: ${storageType}`); + + storageType = storageType || "vanilla"; + + if (storageType === "dexie") { + console.log("Using Dexie.js storage manager"); + return new DexieDBManager(); + } + + // Default to vanilla IndexedDB + console.log("Using vanilla IndexedDB storage manager"); + return new IndexedDBManager(); +} diff --git a/experimental/javascript-wc-indexeddb/src/utils/nanoid.js b/experimental/javascript-wc-indexeddb/src/utils/nanoid.js new file mode 100644 index 000000000..5df154f1f --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/utils/nanoid.js @@ -0,0 +1,41 @@ +/* Borrowed from https://github.com/ai/nanoid/blob/3.0.2/non-secure/index.js + +The MIT License (MIT) + +Copyright 2017 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + +// This alphabet uses `A-Za-z0-9_-` symbols. +// The order of characters is optimized for better gzip and brotli compression. +// References to the same file (works both for gzip and brotli): +// `'use`, `andom`, and `rict'` +// References to the brotli default dictionary: +// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf` +let urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; + +export function nanoid(size = 21) { + let id = ""; + // A compact alternative for `for (var i = 0; i < step; i++)`. + let i = size; + while (i--) { + // `| 0` is more compact and faster than `Math.floor()`. + id += urlAlphabet[(Math.random() * 64) | 0]; + } + return id; +} diff --git a/experimental/javascript-wc-indexeddb/src/workload-test.mjs b/experimental/javascript-wc-indexeddb/src/workload-test.mjs new file mode 100644 index 000000000..da4ddec96 --- /dev/null +++ b/experimental/javascript-wc-indexeddb/src/workload-test.mjs @@ -0,0 +1,84 @@ +import { BenchmarkStep, BenchmarkSuite } from "./speedometer-utils/benchmark.mjs"; +import { getTodoText, defaultLanguage } from "/node_modules/speedometer-utils/translations.mjs"; +import { numberOfItemsToAdd } from "/node_modules/speedometer-utils/todomvc-utils.mjs"; + +export const appName = "todomvc-indexeddb"; +export const appVersion = "1.0.0"; + +const suites = { + default: new BenchmarkSuite("indexeddb", [ + new BenchmarkStep(`Adding${numberOfItemsToAdd}Items`, async () => { + const input = document.querySelector("todo-app").shadowRoot.querySelector("todo-topbar").shadowRoot.querySelector(".new-todo-input"); + for (let i = 0; i < numberOfItemsToAdd; i++) { + input.value = getTodoText(defaultLanguage, i); + input.dispatchEvent(new Event("input")); + input.dispatchEvent(new KeyboardEvent("keyup", { key: "Enter" })); + } + }), + new BenchmarkStep( + "FinishAddingItemsToDB", + async () => { + await window.addPromise; + }, + /* ignoreResult = */ true + ), + new BenchmarkStep("CompletingAllItems", async () => { + const numberOfItemsPerIteration = 10; + const numberOfIterations = 10; + window.numberOfItemsToAdd = numberOfItemsToAdd; + for (let j = 0; j < numberOfIterations; j++) { + const todoList = document.querySelector("todo-app").shadowRoot.querySelector("todo-list"); + const items = todoList.shadowRoot.querySelectorAll("todo-item"); + for (let i = 0; i < numberOfItemsPerIteration; i++) { + const item = items[i].shadowRoot.querySelector(".toggle-todo-input"); + item.click(); + } + if (j < 9) { + const nextPageButton = document.querySelector("todo-app").shadowRoot.querySelector("todo-bottombar").shadowRoot.querySelector(".next-page-button"); + nextPageButton.click(); + } + } + }), + new BenchmarkStep( + "FinishModifyingItemsInDB", + async () => { + await window.togglePromise; + }, + /* ignoreResult = */ true + ), + new BenchmarkStep("DeletingAllItems", async () => { + const numberOfItemsPerIteration = 10; + const numberOfIterations = 10; + window.numberOfItemsToAdd = numberOfItemsToAdd; + function iterationFinishedListener() { + iterationFinishedListener.promiseResolve(); + } + window.addEventListener("previous-page-loaded", iterationFinishedListener); + for (let j = 0; j < numberOfIterations; j++) { + const iterationFinishedPromise = new Promise((resolve) => { + iterationFinishedListener.promiseResolve = resolve; + }); + const todoList = document.querySelector("todo-app").shadowRoot.querySelector("todo-list"); + const items = todoList.shadowRoot.querySelectorAll("todo-item"); + for (let i = numberOfItemsPerIteration - 1; i >= 0; i--) { + const item = items[i].shadowRoot.querySelector(".remove-todo-button"); + item.click(); + } + if (j < 9) { + const previousPageButton = document.querySelector("todo-app").shadowRoot.querySelector("todo-bottombar").shadowRoot.querySelector(".previous-page-button"); + previousPageButton.click(); + await iterationFinishedPromise; + } + } + }), + new BenchmarkStep( + "FinishDeletingItemsFromDB", + async () => { + await window.removePromise; + }, + /* ignoreResult = */ true + ), + ]), +}; + +export default suites; diff --git a/experimental/tests.mjs b/experimental/tests.mjs index ca4fa572b..23ff81d46 100644 --- a/experimental/tests.mjs +++ b/experimental/tests.mjs @@ -142,4 +142,24 @@ export const ExperimentalSuites = freezeSuites([ name: "default", // optional param to target non-default tests locally }, */ }, + { + name: "TodoMVC-WebComponents-IndexedDB", + url: "experimental/javascript-wc-indexeddb/dist/index.html?useAsyncSteps=true&storageType=vanilla", + tags: ["todomvc", "webcomponents", "experimental"], + async prepare() {}, + type: "remote", + /* config: { + name: "default", // optional param to target non-default tests locally + }, */ + }, + { + name: "TodoMVC-WebComponents-DexieJS", + url: "experimental/javascript-wc-indexeddb/dist/index.html?useAsyncSteps=true&storageType=dexie", + tags: ["todomvc", "webcomponents", "experimental"], + async prepare() {}, + type: "remote", + /* config: { + name: "default", // optional param to target non-default tests locally + }, */ + }, ]); diff --git a/resources/suite-runner.mjs b/resources/suite-runner.mjs index f770787e0..5cfc0b50a 100644 --- a/resources/suite-runner.mjs +++ b/resources/suite-runner.mjs @@ -105,7 +105,8 @@ export class SuiteRunner { const frame = this.#frame; frame.onload = () => resolve(); frame.onerror = () => reject(); - frame.src = `${this.#suite.url}?${this.#params.toSearchParams()}`; + const splitUrl = this.#suite.url.split("?"); + frame.src = `${splitUrl[0]}?${splitUrl[1] ?? ""}&${this.#params.toSearchParams()}`; }); }