Skip to content

Commit

Permalink
Chech input array for duplicate primary keys before updating the live…
Browse files Browse the repository at this point in the history
…Query cache.

This commit also introduces RangeSet.hasKey() method (to simplify seeking of a key in a rangeset), corrects the typing of IntervalTreeNode and removes some unused imports.

Resolves #2011
Resolves #2012
  • Loading branch information
dfahlander committed Jul 9, 2024
1 parent 8e0a940 commit 182e6a9
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 26 deletions.
4 changes: 4 additions & 0 deletions src/helpers/rangeset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ props(RangeSet.prototype, {
keys.forEach(key => addRange(this, key, key));
return this;
},
hasKey(key: IndexableType) {
const node = getRangeSetIterator(this).next(key).value;
return node && cmp(node.from, key) <= 0 && cmp(node.to, key) >= 0;
},

[iteratorSymbol](): Iterator<IntervalTreeNode, undefined, IndexableType | undefined> {
return getRangeSetIterator(this);
Expand Down
53 changes: 29 additions & 24 deletions src/live-query/cache/apply-optimistic-ops.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { cmp } from '../../functions/cmp';
import { deepClone, isArray } from '../../functions/utils';
import { RangeSet, rangesOverlap } from '../../helpers/rangeset';
import { isArray } from '../../functions/utils';
import { RangeSet } from '../../helpers/rangeset';
import { CacheEntry } from '../../public/types/cache';
import {
DBCoreIndex,
DBCoreMutateRequest,
DBCoreQueryRequest,
DBCoreTable,
Expand All @@ -29,19 +28,25 @@ export function applyOptimisticOps(

let finalResult = ops.reduce((result, op) => {
let modifedResult = result;
const includedValues =
op.type === 'add' || op.type === 'put'
? op.values.filter((v) => {
const key = extractIndex(v);
return multiEntry && isArray(key) // multiEntry index work like plain index unless key is array
? key.some((k) => isWithinRange(k, queryRange)) // multiEntry and array key
: isWithinRange(key, queryRange); // multiEntry but not array key
}).map(v => {
v = deepClone(v);// v might come from user so we can't just freeze it.
if (immutable) Object.freeze(v);
return v;
})
: [];
const includedValues: any[] = [];
if (op.type === 'add' || op.type === 'put') {
const includedPKs = new RangeSet(); // For ignoring duplicates
for (let i = op.values.length - 1; i >= 0; --i) {
// backwards to prioritize last value of same PK
const value = op.values[i];
const pk = extractPrimKey(value);
if (includedPKs.hasKey(pk)) continue;
const key = extractIndex(value);
if (
multiEntry && isArray(key)
? key.some((k) => isWithinRange(k, queryRange))
: isWithinRange(key, queryRange)
) {
includedPKs.addKey(pk);
includedValues.push(value);
}
}
}
switch (op.type) {
case 'add':
modifedResult = result.concat(
Expand All @@ -55,22 +60,22 @@ export function applyOptimisticOps(
op.values.map((v) => extractPrimKey(v))
);
modifedResult = result
.filter((item) => {
const key = req.values ? extractPrimKey(item) : item;
return !rangesOverlap(new RangeSet(key), keySet);
})
.filter(
// Remove all items that are being replaced
(item) => !keySet.hasKey(req.values ? extractPrimKey(item) : item)
)
.concat(
// Add all items that are being put (sorting will be done later)
req.values
? includedValues
: includedValues.map((v) => extractPrimKey(v))
);
break;
case 'delete':
const keysToDelete = new RangeSet().addKeys(op.keys);
modifedResult = result.filter((item) => {
const key = req.values ? extractPrimKey(item) : item;
return !rangesOverlap(new RangeSet(key), keysToDelete);
});
modifedResult = result.filter(
(item) => !keysToDelete.hasKey(req.values ? extractPrimKey(item) : item)
);

break;
case 'deleteRange':
Expand Down
5 changes: 3 additions & 2 deletions src/public/types/rangeset.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export type IntervalTree = IntervalTreeNode | EmptyRange;
export interface IntervalTreeNode {
from: IndexableType; // lower bound
to: IndexableType; // upper bound
l: IntervalTreeNode | null; // left
r: IntervalTreeNode | null; // right
l?: IntervalTreeNode | null; // left
r?: IntervalTreeNode | null; // right
d: number; // depth
}
export interface EmptyRange {
Expand All @@ -16,6 +16,7 @@ export interface RangeSetPrototype {
add(rangeSet: IntervalTree | {from: IndexableType, to: IndexableType}): RangeSet;
addKey(key: IndexableType): RangeSet;
addKeys(keys: IndexableType[]): RangeSet;
hasKey(key: IndexableType): boolean;
[Symbol.iterator](): Iterator<IntervalTreeNode, undefined, IndexableType | undefined>;
}

Expand Down

0 comments on commit 182e6a9

Please sign in to comment.