Skip to content

Commit 2dd8c79

Browse files
kevin-dpsamwillis
andcommitted
Callback to inform about loadSubset deduplication (#694)
* Callback that informs about deduplicated loadSubset calls * Unit tests for onDeduplicate callback + move dedupe test files to the right folder * changeset * Object params * Updated lockfile * fix lock file --------- Co-authored-by: Sam Willis <[email protected]>
1 parent f66ce6a commit 2dd8c79

File tree

4 files changed

+243
-32
lines changed

4 files changed

+243
-32
lines changed

.changeset/two-lamps-wave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/db": patch
3+
---
4+
5+
Adds an onDeduplicate callback on the DeduplicatedLoadSubset class which is called when a loadSubset call is deduplicated

packages/db/src/query/subset-dedupe.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@ import type { LoadSubsetOptions } from "../types.js"
1212
* Tracks what data has been loaded and avoids redundant calls by applying
1313
* subset logic to predicates.
1414
*
15+
* @param opts - The options for the DeduplicatedLoadSubset
16+
* @param opts.loadSubset - The underlying loadSubset function to wrap
17+
* @param opts.onDeduplicate - An optional callback function that is invoked when a loadSubset call is deduplicated.
18+
* If the call is deduplicated because the requested data is being loaded by an inflight request,
19+
* then this callback is invoked when the inflight request completes successfully and the data is fully loaded.
20+
* This callback is useful if you need to track rows per query, in which case you can't ignore deduplicated calls
21+
* because you need to know which rows were loaded for each query.
1522
* @example
16-
* const dedupe = new DeduplicatedLoadSubset(myLoadSubset)
23+
* const dedupe = new DeduplicatedLoadSubset({ loadSubset: myLoadSubset, onDeduplicate: (opts) => console.log(`Call was deduplicated:`, opts) })
1724
*
1825
* // First call - fetches data
1926
* await dedupe.loadSubset({ where: gt(ref('age'), val(10)) })
@@ -30,6 +37,11 @@ export class DeduplicatedLoadSubset {
3037
options: LoadSubsetOptions
3138
) => true | Promise<void>
3239

40+
// An optional callback function that is invoked when a loadSubset call is deduplicated.
41+
private readonly onDeduplicate:
42+
| ((options: LoadSubsetOptions) => void)
43+
| undefined
44+
3345
// Combined where predicate for all unlimited calls (no limit)
3446
private unlimitedWhere: BasicExpression<boolean> | undefined = undefined
3547

@@ -52,10 +64,12 @@ export class DeduplicatedLoadSubset {
5264
// check if their captured generation matches before updating tracking state
5365
private generation = 0
5466

55-
constructor(
67+
constructor(opts: {
5668
loadSubset: (options: LoadSubsetOptions) => true | Promise<void>
57-
) {
58-
this._loadSubset = loadSubset
69+
onDeduplicate?: (options: LoadSubsetOptions) => void
70+
}) {
71+
this._loadSubset = opts.loadSubset
72+
this.onDeduplicate = opts.onDeduplicate
5973
}
6074

6175
/**
@@ -71,13 +85,15 @@ export class DeduplicatedLoadSubset {
7185
loadSubset = (options: LoadSubsetOptions): true | Promise<void> => {
7286
// If we've loaded all data, everything is covered
7387
if (this.hasLoadedAllData) {
88+
this.onDeduplicate?.(options)
7489
return true
7590
}
7691

7792
// Check against unlimited combined predicate
7893
// If we've loaded all data matching a where clause, we don't need to refetch subsets
7994
if (this.unlimitedWhere !== undefined && options.where !== undefined) {
8095
if (isWhereSubset(options.where, this.unlimitedWhere)) {
96+
this.onDeduplicate?.(options)
8197
return true // Data already loaded via unlimited call
8298
}
8399
}
@@ -89,6 +105,7 @@ export class DeduplicatedLoadSubset {
89105
)
90106

91107
if (alreadyLoaded) {
108+
this.onDeduplicate?.(options)
92109
return true // Already loaded
93110
}
94111
}
@@ -103,7 +120,10 @@ export class DeduplicatedLoadSubset {
103120
// An in-flight call will load data that covers this request
104121
// Return the same promise so this caller waits for the data to load
105122
// The in-flight promise already handles tracking updates when it completes
106-
return matchingInflight.promise
123+
const prom = matchingInflight.promise
124+
// Call `onDeduplicate` when the inflight request has loaded the data
125+
prom.then(() => this.onDeduplicate?.(options)).catch() // ignore errors
126+
return prom
107127
}
108128

109129
// Not fully covered by existing data

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 {

0 commit comments

Comments
 (0)