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;