Skip to content

Commit 01005b6

Browse files
committed
Unit tests for onDeduplicate callback + move dedupe test files to the right folder
1 parent 72c790e commit 01005b6

File tree

2 files changed

+162
-10
lines changed

2 files changed

+162
-10
lines changed

packages/db/tests/predicate-utils.test.ts renamed to packages/db/tests/query/predicate-utils.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import {
66
isWhereSubset,
77
minusWherePredicates,
88
unionWherePredicates,
9-
} from "../src/query/predicate-utils"
10-
import { Func, PropRef, Value } from "../src/query/ir"
11-
import type { BasicExpression, OrderBy, OrderByClause } from "../src/query/ir"
12-
import type { LoadSubsetOptions } from "../src/types"
9+
} from "../../src/query/predicate-utils"
10+
import { Func, PropRef, Value } from "../../src/query/ir"
11+
import type {
12+
BasicExpression,
13+
OrderBy,
14+
OrderByClause,
15+
} from "../../src/query/ir"
16+
import type { LoadSubsetOptions } from "../../src/types"
1317

1418
// Helper functions to build expressions more easily
1519
function ref(path: string | Array<string>): PropRef {

packages/db/tests/subset-dedupe.test.ts renamed to packages/db/tests/query/subset-dedupe.test.ts

Lines changed: 154 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { describe, expect, it } from "vitest"
1+
import { describe, expect, it, vi } from "vitest"
22
import {
33
DeduplicatedLoadSubset,
44
cloneOptions,
5-
} from "../src/query/subset-dedupe"
6-
import { Func, PropRef, Value } from "../src/query/ir"
7-
import { minusWherePredicates } from "../src/query/predicate-utils"
8-
import type { BasicExpression, OrderBy } from "../src/query/ir"
9-
import type { LoadSubsetOptions } from "../src/types"
5+
} from "../../src/query/subset-dedupe"
6+
import { Func, PropRef, Value } from "../../src/query/ir"
7+
import { minusWherePredicates } from "../../src/query/predicate-utils"
8+
import type { BasicExpression, OrderBy } from "../../src/query/ir"
9+
import type { LoadSubsetOptions } from "../../src/types"
1010

1111
// Helper functions to build expressions more easily
1212
function ref(path: string | Array<string>): PropRef {
@@ -559,4 +559,152 @@ describe(`createDeduplicatedLoadSubset`, () => {
559559
*/
560560
})
561561
})
562+
563+
describe(`onDeduplicate callback`, () => {
564+
it(`should call onDeduplicate when all data already loaded`, async () => {
565+
let callCount = 0
566+
const mockLoadSubset = () => {
567+
callCount++
568+
return Promise.resolve()
569+
}
570+
571+
const onDeduplicate = vi.fn()
572+
const deduplicated = new DeduplicatedLoadSubset(
573+
mockLoadSubset,
574+
onDeduplicate
575+
)
576+
577+
// Load all data
578+
await deduplicated.loadSubset({})
579+
expect(callCount).toBe(1)
580+
581+
// Any subsequent request should be deduplicated
582+
const subsetOptions = { where: gt(ref(`age`), val(10)) }
583+
const result = await deduplicated.loadSubset(subsetOptions)
584+
expect(result).toBe(true)
585+
expect(callCount).toBe(1)
586+
expect(onDeduplicate).toHaveBeenCalledTimes(1)
587+
expect(onDeduplicate).toHaveBeenCalledWith(subsetOptions)
588+
})
589+
590+
it(`should call onDeduplicate when unlimited superset already loaded`, async () => {
591+
let callCount = 0
592+
const mockLoadSubset = () => {
593+
callCount++
594+
return Promise.resolve()
595+
}
596+
597+
const onDeduplicate = vi.fn()
598+
const deduplicated = new DeduplicatedLoadSubset(
599+
mockLoadSubset,
600+
onDeduplicate
601+
)
602+
603+
// First call loads a broader set
604+
await deduplicated.loadSubset({ where: gt(ref(`age`), val(10)) })
605+
expect(callCount).toBe(1)
606+
607+
// Second call is a subset of the first; should dedupe and call callback
608+
const subsetOptions = { where: gt(ref(`age`), val(20)) }
609+
const result = await deduplicated.loadSubset(subsetOptions)
610+
expect(result).toBe(true)
611+
expect(callCount).toBe(1)
612+
expect(onDeduplicate).toHaveBeenCalledTimes(1)
613+
expect(onDeduplicate).toHaveBeenCalledWith(subsetOptions)
614+
})
615+
616+
it(`should call onDeduplicate for limited subset requests`, async () => {
617+
let callCount = 0
618+
const mockLoadSubset = () => {
619+
callCount++
620+
return Promise.resolve()
621+
}
622+
623+
const onDeduplicate = vi.fn()
624+
const deduplicated = new DeduplicatedLoadSubset(
625+
mockLoadSubset,
626+
onDeduplicate
627+
)
628+
629+
const orderBy1: OrderBy = [
630+
{
631+
expression: ref(`age`),
632+
compareOptions: {
633+
direction: `asc`,
634+
nulls: `last`,
635+
stringSort: `lexical`,
636+
},
637+
},
638+
]
639+
640+
// First limited call
641+
await deduplicated.loadSubset({
642+
where: gt(ref(`age`), val(10)),
643+
orderBy: orderBy1,
644+
limit: 10,
645+
})
646+
expect(callCount).toBe(1)
647+
648+
// Second limited call is a subset (stricter where and smaller limit)
649+
const subsetOptions = {
650+
where: gt(ref(`age`), val(20)),
651+
orderBy: orderBy1,
652+
limit: 5,
653+
}
654+
const result = await deduplicated.loadSubset(subsetOptions)
655+
expect(result).toBe(true)
656+
expect(callCount).toBe(1)
657+
expect(onDeduplicate).toHaveBeenCalledTimes(1)
658+
expect(onDeduplicate).toHaveBeenCalledWith(subsetOptions)
659+
})
660+
661+
it(`should delay onDeduplicate until covering in-flight request completes`, async () => {
662+
let resolveFirst: (() => void) | undefined
663+
let callCount = 0
664+
const firstPromise = new Promise<void>((resolve) => {
665+
resolveFirst = () => resolve()
666+
})
667+
668+
// First call will remain in-flight until we resolve it
669+
let first = true
670+
const mockLoadSubset = (_options: LoadSubsetOptions) => {
671+
callCount++
672+
if (first) {
673+
first = false
674+
return firstPromise
675+
}
676+
return Promise.resolve()
677+
}
678+
679+
const onDeduplicate = vi.fn()
680+
const deduplicated = new DeduplicatedLoadSubset(
681+
mockLoadSubset,
682+
onDeduplicate
683+
)
684+
685+
// Start a broad in-flight request
686+
const inflightOptions = { where: gt(ref(`age`), val(10)) }
687+
const inflight = deduplicated.loadSubset(inflightOptions)
688+
expect(inflight).toBeInstanceOf(Promise)
689+
expect(callCount).toBe(1)
690+
691+
// Issue a subset request while first is still in-flight
692+
const subsetOptions = { where: gt(ref(`age`), val(20)) }
693+
const subsetPromise = deduplicated.loadSubset(subsetOptions)
694+
expect(subsetPromise).toBeInstanceOf(Promise)
695+
696+
// onDeduplicate should NOT have fired yet
697+
expect(onDeduplicate).not.toHaveBeenCalled()
698+
699+
// Complete the first request
700+
resolveFirst?.()
701+
702+
// Wait for the subset promise to settle (which chains the first)
703+
await subsetPromise
704+
705+
// Now the callback should have been called exactly once, with the subset options
706+
expect(onDeduplicate).toHaveBeenCalledTimes(1)
707+
expect(onDeduplicate).toHaveBeenCalledWith(subsetOptions)
708+
})
709+
})
562710
})

0 commit comments

Comments
 (0)