diff --git a/src/classes/collection/collection.ts b/src/classes/collection/collection.ts index 79abc011d..7a1291d63 100644 --- a/src/classes/collection/collection.ts +++ b/src/classes/collection/collection.ts @@ -12,6 +12,7 @@ import { hangsOnDeleteLargeKeyRange } from "../../globals/constants"; import { ThenShortcut } from "../../public/types/then-shortcut"; import { Transaction } from '../transaction'; import { DBCoreCursor, DBCoreTransaction, DBCoreRangeType, DBCoreMutateResponse, DBCoreKeyRange } from '../../public/types/dbcore'; +import { cmp } from "../../functions/cmp"; /** class Collection * @@ -485,7 +486,6 @@ export class Collection implements ICollection { const coreTable = ctx.table.core; const {outbound, extractKey} = coreTable.schema.primaryKey; const limit = this.db._options.modifyChunkSize || 200; - const {cmp} = this.db.core; const totalFailures = []; let successCount = 0; const failedKeys: IndexableType[] = []; diff --git a/src/classes/dexie/dexie-static-props.ts b/src/classes/dexie/dexie-static-props.ts index f19067677..f56942014 100644 --- a/src/classes/dexie/dexie-static-props.ts +++ b/src/classes/dexie/dexie-static-props.ts @@ -22,6 +22,7 @@ import { globalEvents } from '../../globals/global-events'; import { liveQuery } from '../../live-query/live-query'; import { extendObservabilitySet } from '../../live-query/extend-observability-set'; import { domDeps } from './dexie-dom-dependencies'; +import { cmp } from '../../functions/cmp'; /* (Dexie) is an instance of DexieConstructor, as defined in public/types/dexie-constructor.d.ts * (new Dexie()) is an instance of Dexie, as defined in public/types/dexie.d.ts @@ -186,6 +187,7 @@ props(Dexie, { shallowClone: shallowClone, deepClone: deepClone, getObjectDiff: getObjectDiff, + cmp, asap: asap, //maxKey: new Dexie('',{addons:[]})._maxKey, minKey: minKey, diff --git a/src/classes/dexie/generate-middleware-stacks.ts b/src/classes/dexie/generate-middleware-stacks.ts index 85279c589..456fdc521 100644 --- a/src/classes/dexie/generate-middleware-stacks.ts +++ b/src/classes/dexie/generate-middleware-stacks.ts @@ -18,7 +18,7 @@ function createMiddlewareStacks( tmpTrans: IDBTransaction): {[StackName in keyof DexieStacks]?: DexieStacks[StackName]} { const dbcore = createMiddlewareStack( - createDBCore(idbdb, indexedDB, IDBKeyRange, tmpTrans), + createDBCore(idbdb, IDBKeyRange, tmpTrans), middlewares.dbcore); // TODO: Create other stacks the same way as above. They might be dependant on the result diff --git a/src/dbcore/dbcore-indexeddb.ts b/src/dbcore/dbcore-indexeddb.ts index e774f180b..7a7cbb5b5 100644 --- a/src/dbcore/dbcore-indexeddb.ts +++ b/src/dbcore/dbcore-indexeddb.ts @@ -39,12 +39,9 @@ export function getKeyPathAlias(keyPath: null | string | string[]) { export function createDBCore ( db: IDBDatabase, - indexedDB: IDBFactory, IdbKeyRange: typeof IDBKeyRange, tmpTrans: IDBTransaction) : DBCore { - const cmp = indexedDB.cmp.bind(indexedDB); - function extractSchema(db: IDBDatabase, trans: IDBTransaction) : {schema: DBCoreSchema, hasGetAll: boolean} { const tables = arrayify(db.objectStoreNames); return { @@ -404,8 +401,6 @@ export function createDBCore ( return tableMap[name]; }, - cmp, - MIN_KEY: -Infinity, MAX_KEY: getMaxKey(IdbKeyRange), diff --git a/src/dbcore/keyrange.ts b/src/dbcore/keyrange.ts index af97e2d4f..6ea07a5e8 100644 --- a/src/dbcore/keyrange.ts +++ b/src/dbcore/keyrange.ts @@ -1,12 +1,10 @@ -import { domDeps } from '../classes/dexie/dexie-dom-dependencies'; -import { getMaxKey } from '../functions/quirks'; import { DBCoreKeyRange, DBCoreRangeType } from '../public/types/dbcore'; export const AnyRange: DBCoreKeyRange = { type: DBCoreRangeType.Any, lower: -Infinity, lowerOpen: false, - get upper() { return getMaxKey(domDeps.IDBKeyRange) }, + upper: [[]], upperOpen: false } diff --git a/src/functions/cmp.ts b/src/functions/cmp.ts index 54deb0b58..9b0ec2705 100644 --- a/src/functions/cmp.ts +++ b/src/functions/cmp.ts @@ -1,18 +1,89 @@ -import { domDeps } from '../classes/dexie/dexie-dom-dependencies'; -import { exceptions } from '../errors'; -import { IndexableType } from '../public'; +// Implementation of https://www.w3.org/TR/IndexedDB-3/#compare-two-keys -let _cmp: (a: any, b: any) => number; -export function cmp(a: IndexableType, b: IndexableType): number { - if (_cmp) return _cmp(a, b); - const {indexedDB} = domDeps; - if (!indexedDB) throw new exceptions.MissingAPI(); - _cmp = (a, b) => { - try { - return a == null || b == null ? NaN : indexedDB.cmp(a, b); - } catch { - return NaN; +import { toStringTag } from './utils'; + +// ... with the adjustment to return NaN instead of throwing. +export function cmp(a: any, b: any): number { + 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; // Return value if any given args are valid keys. +} + +export function compareArrays(a: any[], b: any[]): number { + 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; +} + +export function compareUint8Arrays( + a: Uint8Array, + b: Uint8Array +) { + 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 _cmp(a, b); + return al === bl ? 0 : al < bl ? -1 : 1; +} + +// Implementation of https://www.w3.org/TR/IndexedDB-3/#key-type +function type(x: any) { + const t = typeof x; + if (t !== 'object') return t; + if (ArrayBuffer.isView(x)) return 'binary'; + const tsTag = toStringTag(x); // Cannot use instanceof in Safari + return tsTag === 'ArrayBuffer' ? 'binary' : (tsTag as 'Array' | 'Date'); +} + +type BinaryType = + | ArrayBuffer + | DataView + | Uint8ClampedArray + | ArrayBufferView + | Uint8Array + | Int8Array + | Uint16Array + | Int16Array + | Uint32Array + | Int32Array + | Float32Array + | Float64Array; + +function getUint8Array(a: BinaryType): Uint8Array { + if (a instanceof Uint8Array) return a; + if (ArrayBuffer.isView(a)) + // TypedArray or DataView + return new Uint8Array(a.buffer, a.byteOffset, a.byteLength); + return new Uint8Array(a); // ArrayBuffer } diff --git a/src/public/types/dbcore.d.ts b/src/public/types/dbcore.d.ts index 1d623e0f4..918a13d30 100644 --- a/src/public/types/dbcore.d.ts +++ b/src/public/types/dbcore.d.ts @@ -167,7 +167,6 @@ export interface DBCore { transaction(stores: string[], mode: 'readonly' | 'readwrite', options?: DbCoreTransactionOptions): DBCoreTransaction; // Utility methods - cmp(a: any, b: any) : number; readonly MIN_KEY: any; readonly MAX_KEY: any; readonly schema: DBCoreSchema; diff --git a/test/tests-all.js b/test/tests-all.js index e565965b6..e587ae85e 100644 --- a/test/tests-all.js +++ b/test/tests-all.js @@ -1,5 +1,6 @@ import Dexie from 'dexie'; Dexie.test = true; // Improve code coverage +import "./tests-cmp.js"; import "./tests-table.js"; import "./tests-chrome-transaction-durability.js"; import "./tests-collection.js"; diff --git a/test/tests-cmp.js b/test/tests-cmp.js new file mode 100644 index 000000000..a59598d8b --- /dev/null +++ b/test/tests-cmp.js @@ -0,0 +1,150 @@ +import Dexie from 'dexie'; +import { module, test, equal, ok } from 'QUnit'; + +function fillArrayBuffer(ab, val) { + const view = new Uint8Array(ab); + for (let i = 0; i < view.byteLength; ++i) { + view[i] = val; + } +} + +module('cmp'); + +const { cmp } = Dexie; + +test('it should support indexable types', () => { + // numbers + ok(cmp(1, 1) === 0, 'Equal numbers should return 0'); + ok(cmp(1, 2) === -1, 'Less than numbers should return -1'); + ok(cmp(-1, -2000) === 1, 'Greater than numbers should return 1'); + // strings + ok(cmp('A', 'A') === 0, 'Equal strings should return 0'); + ok(cmp('A', 'B') === -1, 'Less than strings should return -1'); + ok(cmp('C', 'A') === 1, 'Greater than strings should return 1'); + // Dates + ok(cmp(new Date(1), new Date(1)) === 0, 'Equal dates should return 0'); + ok(cmp(new Date(1), new Date(2)) === -1, 'Less than dates should return -1'); + ok( + cmp(new Date(1000), new Date(500)) === 1, + 'Greater than dates should return 1' + ); + // Arrays + ok(cmp([1, 2, '3'], [1, 2, '3']) === 0, 'Equal arrays should return 0'); + ok(cmp([-1], [1]) === -1, 'Less than arrays should return -1'); + ok(cmp([1], [-1]) === 1, 'Greater than arrays should return 1'); + ok(cmp([1], [1, 0]) === -1, 'If second array is longer with same leading entries, return -1'); + ok(cmp([1, 0], [1]) === 1, 'If first array is longer with same leading entries, return 1'); + ok(cmp([1], [0,0]) === 1, 'If first array is shorter but has greater leading entries, return 1'); + ok(cmp([0,0], [1]) === -1, 'If second array is shorter but has greater leading entries, return -1'); + + /* Binary types + | DataView + | Uint8ClampedArray + | Uint8Array + | Int8Array + | Uint16Array + | Int16Array + | Uint32Array + | Int32Array + | Float32Array + | Float64Array; +*/ + const viewTypes = [ + 'DataView', + 'Uint8ClampedArray', + 'Uint8Array', + 'Int8Array', + 'Uint16Array', + 'Uint32Array', + 'Int32Array', + 'Float32Array', + 'Float64Array', + ] + .map((typeName) => [typeName, self[typeName]]) + .filter(([_, ctor]) => !!ctor); // Don't try to test types not supported by the browser + + const zeroes1 = new ArrayBuffer(16); + const zeroes2 = new ArrayBuffer(16); + const ones = new ArrayBuffer(16); + fillArrayBuffer(zeroes1, 0); + fillArrayBuffer(zeroes2, 0); + fillArrayBuffer(ones, 1); + + for (const [typeName, ArrayBufferView] of viewTypes) { + // Equals + let v1 = new ArrayBufferView(zeroes1); + let v2 = new ArrayBufferView(zeroes2); + ok(cmp(v1, v2) === 0, `Equal ${typeName}s should return 0`); + // Less than + v1 = new ArrayBufferView(zeroes1); + v2 = new ArrayBufferView(ones); + ok(cmp(v1, v2) === -1, `Less than ${typeName}s should return -1`); + // Less than + v1 = new ArrayBufferView(ones); + v2 = new ArrayBufferView(zeroes1); + ok(cmp(v1, v2) === 1, `Greater than ${typeName}s should return 1`); + } +}); +test("it should respect IndexedDB's type order", () => { + const zoo = [ + 'meow', + 1, + new Date(), + Infinity, + -Infinity, + new ArrayBuffer(1), + [[]], + ]; + const [minusInfinity, num, infinity, date, string, binary, array] = + zoo.sort(cmp); + equal(minusInfinity, -Infinity, 'Minus infinity is sorted first'); + equal(num, 1, 'Numbers are sorted second'); + equal(infinity, Infinity, 'Infinity is sorted third'); + ok(date instanceof Date, 'Date is sorted fourth'); + ok(typeof string === 'string', 'strings are sorted fifth'); + ok(binary instanceof ArrayBuffer, 'binaries are sorted sixth'); + ok(Array.isArray(array), 'Arrays are sorted seventh'); +}); + +test('it should return NaN on invalid types', () => { + ok( + isNaN(cmp(1, { foo: 'bar' })), + 'Comparing a number against an object returns NaN (would throw in indexedDB)' + ); + ok( + isNaN(cmp({ foo: 'bar' }, 1)), + 'Comparing an object against a number returns NaN also' + ); +}); + +test('it should treat different binary types as if they were equal', () => { + const viewTypes = [ + 'DataView', + 'Uint8ClampedArray', + 'Uint8Array', + 'Int8Array', + 'Uint16Array', + 'Uint32Array', + 'Int32Array', + 'Float32Array', + 'Float64Array', + ] + .map((typeName) => [typeName, self[typeName]]) + .filter(([_, ctor]) => !!ctor); // Don't try to test types not supported by the browser + + const zeroes1 = new ArrayBuffer(16); + const zeroes2 = new ArrayBuffer(16); + fillArrayBuffer(zeroes1, 0); + fillArrayBuffer(zeroes2, 0); + + for (const [typeName, ArrayBufferView] of viewTypes) { + let v1 = new ArrayBufferView(zeroes1); + ok(cmp(v1, zeroes1) === 0, `Comparing ${typeName} with ArrayBuffer should return 0 if they have identical data`); + } +}); + +test('it should return NaN if comparing arrays where any item or sub array item includes an invalid key', ()=> { + ok(cmp([1, [[2, "3"]]], [1,[[2, "3"]]]) === 0, "It can deep compare arrays with valid keys (equals)"); + ok(cmp([1, [[2, "3"]]], [1,[[2, 3]]]) === 1, "It can deep compare arrays with valid keys (greater than)"); + ok(isNaN(cmp([1, [[2, 3]]], [1,[[{foo: "bar"}, 3]]])), "It returns NaN when any item in the any of the arrays are invalid keys"); +}); diff --git a/test/tests-misc.js b/test/tests-misc.js index 300fdff0e..120b03346 100644 --- a/test/tests-misc.js +++ b/test/tests-misc.js @@ -1,6 +1,6 @@ import Dexie from 'dexie'; import {module, stop, start, asyncTest, equal, deepEqual, ok} from 'QUnit'; -import {resetDatabase, spawnedTest, promisedTest, supports} from './dexie-unittest-utils'; +import {resetDatabase, spawnedTest, promisedTest, supports, isIE, isEdge} from './dexie-unittest-utils'; const async = Dexie.async; @@ -321,6 +321,11 @@ asyncTest ("#1079 mapToClass", function(){ }); asyncTest("PR #1108", async ()=>{ + if (isIE || isEdge) { + ok(true, "Disabling this test for IE and legacy Edge"); + start(); + return; + } const origConsoleWarn = console.warn; const warnings = []; console.warn = function(msg){warnings.push(msg); return origConsoleWarn.apply(this, arguments)};