mirror of
https://github.com/zero-peak/ZeroOmega.git
synced 2025-01-22 23:08:13 -05:00
471 lines
15 KiB
JavaScript
471 lines
15 KiB
JavaScript
|
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;
|