-
-
Notifications
You must be signed in to change notification settings - Fork 626
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Switch to JS implementation of indexedDB.cmp(). (#1412)
* Switch to JS implementation of indexedDB.cmp(). This one is about 30 times faster than indexedDB.cmp() + it removes the global dependency of indexedDB for RangeSet, used by liveQuery(). * Bugfixed new unit test for cmp * Removed alway-false condition. * Added tests to comply to IDB spec: Comparing two arrays with different length: https://www.w3.org/TR/IndexedDB-3/#compare-two-keys * Bugfixes spec compliancy: comparing arrays and typed arrays of different sizes. * Bugfix: treated NaN array items as equals. * Removing another domDeps dependent code part. Ran unit tests on IE after doing this. All still pass. Though one test on Dexie.Observable failed so I without digging to deep into the reason, I disabled that test for IE and legacy edge.
- Loading branch information
1 parent
6a227cf
commit 46a430f
Showing
10 changed files
with
247 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters