ZeroOmega/omega-web/lib/fake-indexeddb/FDBCursor.js

471 lines
15 KiB
JavaScript
Raw Normal View History

import FDBKeyRange from "./FDBKeyRange.js";
import FDBObjectStore from "./FDBObjectStore.js";
import cmp from "./lib/cmp.js";
import { DataError, InvalidAccessError, InvalidStateError, ReadOnlyError, TransactionInactiveError } from "./lib/errors.js";
import extractKey from "./lib/extractKey.js";
import valueToKey from "./lib/valueToKey.js";
const getEffectiveObjectStore = cursor => {
if (cursor.source instanceof FDBObjectStore) {
return cursor.source;
}
return cursor.source.objectStore;
};
// This takes a key range, a list of lower bounds, and a list of upper bounds and combines them all into a single key
// range. It does not handle gt/gte distinctions, because it doesn't really matter much anyway, since for next/prev
// cursor iteration it'd also have to look at values to be precise, which would be complicated. This should get us 99%
// of the way there.
const makeKeyRange = (range, lowers, uppers) => {
// Start with bounds from range
let lower = range !== undefined ? range.lower : undefined;
let upper = range !== undefined ? range.upper : undefined;
// Augment with values from lowers and uppers
for (const lowerTemp of lowers) {
if (lowerTemp === undefined) {
continue;
}
if (lower === undefined || cmp(lower, lowerTemp) === 1) {
lower = lowerTemp;
}
}
for (const upperTemp of uppers) {
if (upperTemp === undefined) {
continue;
}
if (upper === undefined || cmp(upper, upperTemp) === -1) {
upper = upperTemp;
}
}
if (lower !== undefined && upper !== undefined) {
return FDBKeyRange.bound(lower, upper);
}
if (lower !== undefined) {
return FDBKeyRange.lowerBound(lower);
}
if (upper !== undefined) {
return FDBKeyRange.upperBound(upper);
}
};
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor
class FDBCursor {
_gotValue = false;
_position = undefined; // Key of previously returned record
_objectStorePosition = undefined;
_keyOnly = false;
_key = undefined;
_primaryKey = undefined;
constructor(source, range, direction = "next", request, keyOnly = false) {
this._range = range;
this._source = source;
this._direction = direction;
this._request = request;
this._keyOnly = keyOnly;
}
// Read only properties
get source() {
return this._source;
}
set source(val) {
/* For babel */
}
get request() {
return this._request;
}
set request(val) {
/* For babel */
}
get direction() {
return this._direction;
}
set direction(val) {
/* For babel */
}
get key() {
return this._key;
}
set key(val) {
/* For babel */
}
get primaryKey() {
return this._primaryKey;
}
set primaryKey(val) {
/* For babel */
}
// https://w3c.github.io/IndexedDB/#iterate-a-cursor
_iterate(key, primaryKey) {
const sourceIsObjectStore = this.source instanceof FDBObjectStore;
// Can't use sourceIsObjectStore because TypeScript
const records = this.source instanceof FDBObjectStore ? this.source._rawObjectStore.records : this.source._rawIndex.records;
let foundRecord;
if (this.direction === "next") {
const range = makeKeyRange(this._range, [key, this._position], []);
for (const record of records.values(range)) {
const cmpResultKey = key !== undefined ? cmp(record.key, key) : undefined;
const cmpResultPosition = this._position !== undefined ? cmp(record.key, this._position) : undefined;
if (key !== undefined) {
if (cmpResultKey === -1) {
continue;
}
}
if (primaryKey !== undefined) {
if (cmpResultKey === -1) {
continue;
}
const cmpResultPrimaryKey = cmp(record.value, primaryKey);
if (cmpResultKey === 0 && cmpResultPrimaryKey === -1) {
continue;
}
}
if (this._position !== undefined && sourceIsObjectStore) {
if (cmpResultPosition !== 1) {
continue;
}
}
if (this._position !== undefined && !sourceIsObjectStore) {
if (cmpResultPosition === -1) {
continue;
}
if (cmpResultPosition === 0 && cmp(record.value, this._objectStorePosition) !== 1) {
continue;
}
}
if (this._range !== undefined) {
if (!this._range.includes(record.key)) {
continue;
}
}
foundRecord = record;
break;
}
} else if (this.direction === "nextunique") {
// This could be done without iterating, if the range was defined slightly better (to handle gt/gte cases).
// But the performance difference should be small, and that wouldn't work anyway for directions where the
// value needs to be used (like next and prev).
const range = makeKeyRange(this._range, [key, this._position], []);
for (const record of records.values(range)) {
if (key !== undefined) {
if (cmp(record.key, key) === -1) {
continue;
}
}
if (this._position !== undefined) {
if (cmp(record.key, this._position) !== 1) {
continue;
}
}
if (this._range !== undefined) {
if (!this._range.includes(record.key)) {
continue;
}
}
foundRecord = record;
break;
}
} else if (this.direction === "prev") {
const range = makeKeyRange(this._range, [], [key, this._position]);
for (const record of records.values(range, "prev")) {
const cmpResultKey = key !== undefined ? cmp(record.key, key) : undefined;
const cmpResultPosition = this._position !== undefined ? cmp(record.key, this._position) : undefined;
if (key !== undefined) {
if (cmpResultKey === 1) {
continue;
}
}
if (primaryKey !== undefined) {
if (cmpResultKey === 1) {
continue;
}
const cmpResultPrimaryKey = cmp(record.value, primaryKey);
if (cmpResultKey === 0 && cmpResultPrimaryKey === 1) {
continue;
}
}
if (this._position !== undefined && sourceIsObjectStore) {
if (cmpResultPosition !== -1) {
continue;
}
}
if (this._position !== undefined && !sourceIsObjectStore) {
if (cmpResultPosition === 1) {
continue;
}
if (cmpResultPosition === 0 && cmp(record.value, this._objectStorePosition) !== -1) {
continue;
}
}
if (this._range !== undefined) {
if (!this._range.includes(record.key)) {
continue;
}
}
foundRecord = record;
break;
}
} else if (this.direction === "prevunique") {
let tempRecord;
const range = makeKeyRange(this._range, [], [key, this._position]);
for (const record of records.values(range, "prev")) {
if (key !== undefined) {
if (cmp(record.key, key) === 1) {
continue;
}
}
if (this._position !== undefined) {
if (cmp(record.key, this._position) !== -1) {
continue;
}
}
if (this._range !== undefined) {
if (!this._range.includes(record.key)) {
continue;
}
}
tempRecord = record;
break;
}
if (tempRecord) {
foundRecord = records.get(tempRecord.key);
}
}
let result;
if (!foundRecord) {
this._key = undefined;
if (!sourceIsObjectStore) {
this._objectStorePosition = undefined;
}
// "this instanceof FDBCursorWithValue" would be better and not require (this as any), but causes runtime
// error due to circular dependency.
if (!this._keyOnly && this.toString() === "[object IDBCursorWithValue]") {
this.value = undefined;
}
result = null;
} else {
this._position = foundRecord.key;
if (!sourceIsObjectStore) {
this._objectStorePosition = foundRecord.value;
}
this._key = foundRecord.key;
if (sourceIsObjectStore) {
this._primaryKey = structuredClone(foundRecord.key);
if (!this._keyOnly && this.toString() === "[object IDBCursorWithValue]") {
this.value = structuredClone(foundRecord.value);
}
} else {
this._primaryKey = structuredClone(foundRecord.value);
if (!this._keyOnly && this.toString() === "[object IDBCursorWithValue]") {
if (this.source instanceof FDBObjectStore) {
// Can't use sourceIsObjectStore because TypeScript
throw new Error("This should never happen");
}
const value = this.source.objectStore._rawObjectStore.getValue(foundRecord.value);
this.value = structuredClone(value);
}
}
this._gotValue = true;
// eslint-disable-next-line @typescript-eslint/no-this-alias
result = this;
}
return result;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-update-IDBRequest-any-value
update(value) {
if (value === undefined) {
throw new TypeError();
}
const effectiveObjectStore = getEffectiveObjectStore(this);
const effectiveKey = Object.hasOwn(this.source, "_rawIndex") ? this.primaryKey : this._position;
const transaction = effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (transaction.mode === "readonly") {
throw new ReadOnlyError();
}
if (effectiveObjectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (!(this.source instanceof FDBObjectStore) && this.source._rawIndex.deleted) {
throw new InvalidStateError();
}
if (!this._gotValue || !Object.hasOwn(this, "value")) {
throw new InvalidStateError();
}
const clone = structuredClone(value);
if (effectiveObjectStore.keyPath !== null) {
let tempKey;
try {
tempKey = extractKey(effectiveObjectStore.keyPath, clone);
} catch (err) {
/* Handled immediately below */
}
if (cmp(tempKey, effectiveKey) !== 0) {
throw new DataError();
}
}
const record = {
key: effectiveKey,
value: clone
};
return transaction._execRequestAsync({
operation: effectiveObjectStore._rawObjectStore.storeRecord.bind(effectiveObjectStore._rawObjectStore, record, false, transaction._rollbackLog),
source: this
});
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-advance-void-unsigned-long-count
advance(count) {
if (!Number.isInteger(count) || count <= 0) {
throw new TypeError();
}
const effectiveObjectStore = getEffectiveObjectStore(this);
const transaction = effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (effectiveObjectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (!(this.source instanceof FDBObjectStore) && this.source._rawIndex.deleted) {
throw new InvalidStateError();
}
if (!this._gotValue) {
throw new InvalidStateError();
}
if (this._request) {
this._request.readyState = "pending";
}
transaction._execRequestAsync({
operation: () => {
let result;
for (let i = 0; i < count; i++) {
result = this._iterate();
// Not sure why this is needed
if (!result) {
break;
}
}
return result;
},
request: this._request,
source: this.source
});
this._gotValue = false;
}
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#widl-IDBCursor-continue-void-any-key
continue(key) {
const effectiveObjectStore = getEffectiveObjectStore(this);
const transaction = effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (effectiveObjectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (!(this.source instanceof FDBObjectStore) && this.source._rawIndex.deleted) {
throw new InvalidStateError();
}
if (!this._gotValue) {
throw new InvalidStateError();
}
if (key !== undefined) {
key = valueToKey(key);
const cmpResult = cmp(key, this._position);
if (cmpResult <= 0 && (this.direction === "next" || this.direction === "nextunique") || cmpResult >= 0 && (this.direction === "prev" || this.direction === "prevunique")) {
throw new DataError();
}
}
if (this._request) {
this._request.readyState = "pending";
}
transaction._execRequestAsync({
operation: this._iterate.bind(this, key),
request: this._request,
source: this.source
});
this._gotValue = false;
}
// hthttps://w3c.github.io/IndexedDB/#dom-idbcursor-continueprimarykey
continuePrimaryKey(key, primaryKey) {
const effectiveObjectStore = getEffectiveObjectStore(this);
const transaction = effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (effectiveObjectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (!(this.source instanceof FDBObjectStore) && this.source._rawIndex.deleted) {
throw new InvalidStateError();
}
if (this.source instanceof FDBObjectStore || this.direction !== "next" && this.direction !== "prev") {
throw new InvalidAccessError();
}
if (!this._gotValue) {
throw new InvalidStateError();
}
// Not sure about this
if (key === undefined || primaryKey === undefined) {
throw new DataError();
}
key = valueToKey(key);
const cmpResult = cmp(key, this._position);
if (cmpResult === -1 && this.direction === "next" || cmpResult === 1 && this.direction === "prev") {
throw new DataError();
}
const cmpResult2 = cmp(primaryKey, this._objectStorePosition);
if (cmpResult === 0) {
if (cmpResult2 <= 0 && this.direction === "next" || cmpResult2 >= 0 && this.direction === "prev") {
throw new DataError();
}
}
if (this._request) {
this._request.readyState = "pending";
}
transaction._execRequestAsync({
operation: this._iterate.bind(this, key, primaryKey),
request: this._request,
source: this.source
});
this._gotValue = false;
}
delete() {
const effectiveObjectStore = getEffectiveObjectStore(this);
const effectiveKey = Object.hasOwn(this.source, "_rawIndex") ? this.primaryKey : this._position;
const transaction = effectiveObjectStore.transaction;
if (transaction._state !== "active") {
throw new TransactionInactiveError();
}
if (transaction.mode === "readonly") {
throw new ReadOnlyError();
}
if (effectiveObjectStore._rawObjectStore.deleted) {
throw new InvalidStateError();
}
if (!(this.source instanceof FDBObjectStore) && this.source._rawIndex.deleted) {
throw new InvalidStateError();
}
if (!this._gotValue || !Object.hasOwn(this, "value")) {
throw new InvalidStateError();
}
return transaction._execRequestAsync({
operation: effectiveObjectStore._rawObjectStore.deleteRecord.bind(effectiveObjectStore._rawObjectStore, effectiveKey, transaction._rollbackLog),
source: this
});
}
toString() {
return "[object IDBCursor]";
}
}
export default FDBCursor;