taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit d4d3b5c989f4a522a5355dadf70c98d7c4077c14
parent f7582fd34f26ccdedac6ec391d503c3f8b699abe
Author: Florian Dold <florian@dold.me>
Date:   Tue,  3 Feb 2026 19:02:52 +0100

idb-bridge: fix/simplify index iteration

Diffstat:
Mpackages/idb-bridge/src/MemoryBackend.ts | 600+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mpackages/idb-bridge/src/SqliteBackend.ts | 4++--
Mpackages/idb-bridge/src/testingdb.ts | 3+--
3 files changed, 484 insertions(+), 123 deletions(-)

diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts @@ -1592,6 +1592,427 @@ export class MemoryBackend implements Backend { } } +interface Boundary { + key: Key; + inclusive: boolean; +} + +function getRangeEndBoundary( + forward: boolean, + range: IDBKeyRange | undefined | null, +): Boundary | undefined { + let endRangeKey: Uint8Array | undefined = undefined; + let endRangeInclusive: boolean = false; + if (range) { + if (forward && range.upper != null) { + endRangeKey = range.upper; + endRangeInclusive = !range.upperOpen; + } else if (!forward && range.lower != null) { + endRangeKey = range.lower; + endRangeInclusive = !range.lowerOpen; + } + } + if (endRangeKey) { + return { + inclusive: endRangeInclusive, + key: endRangeKey, + }; + } + return undefined; +} + +function isOutsideBoundary( + forward: boolean, + endRange: Boundary, + currentKey: Key, +): boolean { + const cmp = compareKeys(currentKey, endRange.key); + if (forward && endRange.inclusive && cmp > 0) { + return true; + } else if (forward && !endRange.inclusive && cmp >= 0) { + return true; + } else if (!forward && endRange.inclusive && cmp < 0) { + return true; + } else if (!forward && !endRange.inclusive && cmp <= 0) { + return true; + } + return false; +} + +interface IndexIterPos { + objectPos: Key; + indexPos: Key; +} + +type IndexData = ISortedMapF<IDBValidKey, IndexRecord>; + +function startIndex(req: { + indexData: IndexData; + forward: boolean; + unique: boolean; +}): IndexIterPos | undefined { + const { forward, unique, indexData } = req; + if (forward) { + const indexPos = indexData.minKey(); + if (indexPos != null) { + const indexEntry = indexData.get(indexPos); + if (!indexEntry) { + throw Error("data error"); + } + const objectPos = indexEntry.primaryKeys.minKey(); + if (objectPos != null) { + return { indexPos, objectPos }; + } + } + } else { + const indexPos = indexData.maxKey(); + if (indexPos != null) { + const indexEntry = indexData.get(indexPos); + if (!indexEntry) { + throw Error("data error"); + } + let objectPos: Key | undefined; + if (unique) { + objectPos = indexEntry.primaryKeys.minKey(); + } else { + objectPos = indexEntry.primaryKeys.maxKey(); + } + + if (objectPos != null) { + return { indexPos, objectPos }; + } + } + } + return undefined; +} + +function continueIndexForwardInclusive( + indexData: IndexData, + targetIndexKey: Key, + targetObjectKey: Key, +): IndexIterPos | undefined { + let indexEntry = indexData.get(targetIndexKey); + if (indexEntry) { + if (indexEntry.primaryKeys.has(targetObjectKey)) { + return { + indexPos: targetIndexKey, + objectPos: targetObjectKey, + }; + } else { + const objectPos = indexEntry.primaryKeys.nextHigherKey(targetObjectKey); + if (!objectPos) { + return undefined; + } + return { indexPos: targetIndexKey, objectPos }; + } + } + + const pair = indexData.nextHigherPair(targetIndexKey); + if (!pair) { + return undefined; + } + + indexEntry = pair[1]; + const indexPos = pair[0]; + + const objectPos = indexEntry.primaryKeys.minKey(); + if (!objectPos) { + return undefined; + } + + return { indexPos, objectPos }; +} + +function continueIndexForwardInclusiveUnique( + indexData: IndexData, + targetIndexKey: Key, +): IndexIterPos | undefined { + const indexEntry = indexData.get(targetIndexKey); + if (indexEntry) { + const objectPos = indexEntry.primaryKeys.minKey(); + if (!objectPos) { + return undefined; + } + return { + indexPos: targetIndexKey, + objectPos, + }; + } + const pair = indexData.nextHigherPair(targetIndexKey); + if (!pair) { + return undefined; + } + const objectPos = pair[1].primaryKeys.minKey(); + if (!objectPos) { + return undefined; + } + return { + indexPos: pair[0], + objectPos, + }; +} + +function continueIndexForwardStrict( + indexData: IndexData, + targetIndexKey: Key, + targetObjectKey: Key, +): IndexIterPos | undefined { + let indexEntry = indexData.get(targetIndexKey); + if (indexEntry) { + const objectPos = indexEntry.primaryKeys.nextHigherKey(targetObjectKey); + if (!objectPos) { + return undefined; + } + return { indexPos: targetIndexKey, objectPos }; + } + + const pair = indexData.nextHigherPair(targetIndexKey); + if (!pair) { + return undefined; + } + + indexEntry = pair[1]; + const indexPos = pair[0]; + + const objectPos = indexEntry.primaryKeys.minKey(); + if (!objectPos) { + return undefined; + } + + return { indexPos, objectPos }; +} + +function continueIndexForwardStrictUnique( + indexData: IndexData, + targetIndexKey: Key, +): IndexIterPos | undefined { + const pair = indexData.nextHigherPair(targetIndexKey); + if (!pair) { + return undefined; + } + const objectPos = pair[1].primaryKeys.minKey(); + if (!objectPos) { + return undefined; + } + return { + indexPos: pair[0], + objectPos, + }; +} + +function continueIndexBackwardInclusive( + indexData: IndexData, + targetIndexKey: Key, + targetObjectKey: Key, +): IndexIterPos | undefined { + let indexEntry = indexData.get(targetIndexKey); + if (indexEntry) { + if (indexEntry.primaryKeys.has(targetObjectKey)) { + return { + indexPos: targetIndexKey, + objectPos: targetObjectKey, + }; + } else { + const objectPos = indexEntry.primaryKeys.nextLowerKey(targetObjectKey); + if (!objectPos) { + return undefined; + } + return { indexPos: targetIndexKey, objectPos }; + } + } + + const pair = indexData.nextLowerPair(targetIndexKey); + if (!pair) { + return undefined; + } + + indexEntry = pair[1]; + const indexPos = pair[0]; + + const objectPos = indexEntry.primaryKeys.maxKey(); + if (!objectPos) { + return undefined; + } + + return { indexPos, objectPos }; +} + +function continueIndexBackwardInclusiveUnique( + indexData: IndexData, + targetIndexKey: Key, +): IndexIterPos | undefined { + const indexEntry = indexData.get(targetIndexKey); + if (indexEntry) { + // Backwards iteration, but still use minKey for object pos + const objectPos = indexEntry.primaryKeys.minKey(); + if (!objectPos) { + return undefined; + } + return { + indexPos: targetIndexKey, + objectPos, + }; + } + const pair = indexData.nextLowerPair(targetIndexKey); + if (!pair) { + return undefined; + } + // Backwards iteration, but still use minKey for object pos + const objectPos = pair[1].primaryKeys.minKey(); + if (!objectPos) { + return undefined; + } + return { + indexPos: pair[0], + objectPos, + }; +} + +function continueIndexBackwardStrict( + indexData: IndexData, + targetIndexKey: Key, + targetObjectKey: Key, +): IndexIterPos | undefined { + let indexEntry = indexData.get(targetIndexKey); + if (indexEntry) { + const objectPos = indexEntry.primaryKeys.nextLowerKey(targetObjectKey); + if (!objectPos) { + return undefined; + } + return { indexPos: targetIndexKey, objectPos }; + } + + const pair = indexData.nextLowerPair(targetIndexKey); + if (!pair) { + return undefined; + } + + indexEntry = pair[1]; + const indexPos = pair[0]; + + const objectPos = indexEntry.primaryKeys.maxKey(); + if (!objectPos) { + return undefined; + } + + return { indexPos, objectPos }; +} + +function continueIndexBackwardStrictUnique( + indexData: IndexData, + targetIndexKey: Key, +): IndexIterPos | undefined { + const pair = indexData.nextLowerPair(targetIndexKey); + if (!pair) { + return undefined; + } + const objectPos = pair[1].primaryKeys.minKey(); + if (!objectPos) { + return undefined; + } + return { + indexPos: pair[0], + objectPos, + }; +} + +/** + * Continue past targetIndexKey (and optionally targetObjectKey) + * in the direction specified by "forward". + * Do nothing if the current position is already past the + * target position. + */ +function continueIndex(req: { + indexData: ISortedMapF<IDBValidKey, IndexRecord>; + currentPos?: IndexIterPos; + unique: boolean; + inclusive: boolean; + forward: boolean; + targetIndexKey: Key; + targetObjectKey?: Key; +}): IndexIterPos | undefined { + const currentPos = req.currentPos; + const forward = req.forward; + const inclusive = req.inclusive; + const dir = forward ? 1 : -1; + if (currentPos) { + // Check that the target position after the current position. + // If not, we just stay at the current position. + const indexCmp = compareKeys(currentPos.indexPos, req.targetIndexKey); + if (dir * indexCmp > 0) { + return currentPos; + } + if (indexCmp === 0) { + if (req.targetObjectKey != null) { + const objectCmp = compareKeys( + currentPos.objectPos, + req.targetObjectKey, + ); + if (req.inclusive && objectCmp === 0) { + return currentPos; + } + if (dir * objectCmp > 0) { + return currentPos; + } + } else if (req.inclusive) { + return currentPos; + } + } + } + const indexData = req.indexData; + if (forward) { + if (req.unique || req.targetObjectKey == null) { + if (inclusive) { + return continueIndexForwardInclusiveUnique( + indexData, + req.targetIndexKey, + ); + } else { + return continueIndexForwardStrictUnique(indexData, req.targetIndexKey); + } + } else { + if (inclusive) { + return continueIndexForwardInclusive( + indexData, + req.targetIndexKey, + req.targetObjectKey, + ); + } else { + return continueIndexForwardStrict( + indexData, + req.targetIndexKey, + req.targetObjectKey, + ); + } + } + } else { + if (req.unique || req.targetObjectKey == null) { + if (inclusive) { + return continueIndexBackwardInclusiveUnique( + indexData, + req.targetIndexKey, + ); + } else { + return continueIndexBackwardStrictUnique(indexData, req.targetIndexKey); + } + } else { + if (inclusive) { + return continueIndexBackwardInclusive( + indexData, + req.targetIndexKey, + req.targetObjectKey, + ); + } else { + return continueIndexBackwardStrict( + indexData, + req.targetIndexKey, + req.targetObjectKey, + ); + } + } + } +} + function getIndexRecords(req: { indexData: ISortedMapF<IDBValidKey, IndexRecord>; storeData: ISortedMapF<IDBValidKey, ObjectStoreRecord>; @@ -1609,14 +2030,7 @@ function getIndexRecords(req: { const indexKeys: Key[] = []; const primaryKeys: Key[] = []; const values: Value[] = []; - const { unique, range, forward, indexData } = req; - - function nextIndexEntry(prevPos: IDBValidKey): IndexRecord | undefined { - const res: [IDBValidKey, IndexRecord] | undefined = forward - ? indexData.nextHigherPair(prevPos) - : indexData.nextLowerPair(prevPos); - return res ? res[1] : undefined; - } + const { unique, forward, indexData } = req; function packResult(): RecordGetResponse { // Collect the values based on the primary keys, @@ -1642,145 +2056,93 @@ function getIndexRecords(req: { }; } - let firstIndexPos = req.lastIndexPosition; - { - const rangeStart = forward ? range.lower : range.upper; - const dataStart = forward ? indexData.minKey() : indexData.maxKey(); - firstIndexPos = furthestKey(forward, firstIndexPos, rangeStart); - firstIndexPos = furthestKey(forward, firstIndexPos, dataStart); - } - - if (firstIndexPos == null) { - return packResult(); - } - - let objectStorePos: IDBValidKey | undefined = undefined; - let indexEntry: IndexRecord | undefined = undefined; + const endRange = getRangeEndBoundary(forward, req.range); - // Now we align at indexPos and after objectStorePos + let currentPos = startIndex({ + forward, + indexData, + unique, + }); - indexEntry = indexData.get(firstIndexPos); - if (!indexEntry) { - // We're not aligned to an index key, go to next index entry - indexEntry = nextIndexEntry(firstIndexPos); - if (!indexEntry) { - return packResult(); - } - objectStorePos = nextKey(true, indexEntry.primaryKeys, undefined); - } else if ( - req.lastIndexPosition != null && - compareKeys(req.lastIndexPosition, indexEntry.indexKey) !== 0 - ) { - // We're already past the desired lastIndexPosition, don't use - // lastObjectStorePosition. - objectStorePos = nextKey(true, indexEntry.primaryKeys, undefined); - } else { - objectStorePos = nextKey( - true, - indexEntry.primaryKeys, - req.lastObjectStorePosition, - ); + if (currentPos == null) { + return packResult(); } - // Now skip lower/upper bound of open ranges - - if ( - forward && - range.lowerOpen && - range.lower != null && - compareKeys(range.lower, indexEntry.indexKey) === 0 - ) { - indexEntry = nextIndexEntry(indexEntry.indexKey); - if (!indexEntry) { - return packResult(); - } - objectStorePos = indexEntry.primaryKeys.minKey(); - } + if (req.advanceIndexKey != null) { + currentPos = continueIndex({ + indexData, + unique, + inclusive: true, + currentPos, + forward, + targetIndexKey: req.advanceIndexKey, + targetObjectKey: req.advancePrimaryKey, + }); - if ( - !forward && - range.upperOpen && - range.upper != null && - compareKeys(range.upper, indexEntry.indexKey) === 0 - ) { - indexEntry = nextIndexEntry(indexEntry.indexKey); - if (!indexEntry) { + if (currentPos == null) { return packResult(); } - objectStorePos = indexEntry.primaryKeys.minKey(); } - // If requested, return only unique results - - if ( - unique && - req.lastIndexPosition != null && - compareKeys(indexEntry.indexKey, req.lastIndexPosition) === 0 - ) { - indexEntry = nextIndexEntry(indexEntry.indexKey); - if (!indexEntry) { + if (req.lastIndexPosition != null) { + currentPos = continueIndex({ + indexData, + unique, + inclusive: false, + currentPos, + forward, + targetIndexKey: req.lastIndexPosition, + targetObjectKey: req.lastObjectStorePosition, + }); + if (!currentPos) { return packResult(); } - objectStorePos = indexEntry.primaryKeys.minKey(); } - if (req.advanceIndexKey != null) { - const ik = furthestKey(forward, indexEntry.indexKey, req.advanceIndexKey)!; - indexEntry = indexData.get(ik); - if (!indexEntry) { - indexEntry = nextIndexEntry(ik); + if (req.range != null) { + const targetKeyObj = forward ? req.range.lower : req.range.upper; + if (targetKeyObj != null) { + const targetKey = targetKeyObj; + const inclusive = forward ? !req.range.lowerOpen : !req.range.upperOpen; + currentPos = continueIndex({ + indexData, + unique, + inclusive, + currentPos, + forward, + targetIndexKey: targetKey, + }); } - if (!indexEntry) { + if (!currentPos) { return packResult(); } } - // Use advancePrimaryKey if necessary - if ( - req.advanceIndexKey != null && - req.advancePrimaryKey && - compareKeys(indexEntry.indexKey, req.advanceIndexKey) == 0 - ) { - if ( - objectStorePos == null || - compareKeys(req.advancePrimaryKey, objectStorePos) > 0 - ) { - objectStorePos = nextKey( - true, - indexEntry.primaryKeys, - req.advancePrimaryKey, - ); - } - } - while (1) { if (req.limit != 0 && numResults == req.limit) { break; } - if (!range.includes(indexEntry.indexKey)) { + if (currentPos == null) { break; } - if (indexEntry === undefined) { + if (endRange && isOutsideBoundary(forward, endRange, currentPos.indexPos)) { break; } - if (objectStorePos == null) { - // We don't have any more records with the current index key. - indexEntry = nextIndexEntry(indexEntry.indexKey); - if (!indexEntry) { - return packResult(); - } - objectStorePos = indexEntry.primaryKeys.minKey(); - continue; - } - indexKeys.push(structuredClone(indexEntry.indexKey)); - primaryKeys.push(structuredClone(objectStorePos)); + indexKeys.push(structuredClone(currentPos.indexPos)); + primaryKeys.push(structuredClone(currentPos.objectPos)); + numResults++; - if (unique) { - objectStorePos = undefined; - } else { - objectStorePos = indexEntry.primaryKeys.nextHigherKey(objectStorePos); - } + + currentPos = continueIndex({ + indexData, + unique, + forward, + inclusive: false, + currentPos: undefined, + targetIndexKey: currentPos.indexPos, + targetObjectKey: currentPos.objectPos, + }); } return packResult(); diff --git a/packages/idb-bridge/src/SqliteBackend.ts b/packages/idb-bridge/src/SqliteBackend.ts @@ -416,7 +416,7 @@ export class SqliteBackend implements Backend { console.log(`objectKey:`, deserializeKey(currentPos.objectPos)); } - if (req.advanceIndexKey) { + if (req.advanceIndexKey != null) { const advanceIndexKey = serializeKey(req.advanceIndexKey); const advancePrimaryKey = req.advancePrimaryKey ? serializeKey(req.advancePrimaryKey) @@ -436,7 +436,7 @@ export class SqliteBackend implements Backend { } } - if (req.lastIndexPosition) { + if (req.lastIndexPosition != null) { if (this.enableTracing) { console.log("index query: seeking past last index position"); console.log("lastObjectPosition", req.lastObjectStorePosition); diff --git a/packages/idb-bridge/src/testingdb.ts b/packages/idb-bridge/src/testingdb.ts @@ -27,8 +27,7 @@ export async function initTestIndexedDB(): Promise<void> { if (process.env.IDB_TEST_BACKEND == "memory") { const memBackend = new MemoryBackend(); backend = memBackend; - // backend.enableTracing = true; - memBackend.enableTracing = false; + memBackend.enableTracing = true; } else { const sqlite3Impl = await createNodeHelperSqlite3Impl({ enableTracing: false,