Skip to content

Commit 6df29a2

Browse files
committed
refactor(db): simplify indexing with BasicIndex and explicit defaultIndexType
- Add BasicIndex using Map + sorted Array for both equality and range queries - Remove registry pattern - pass defaultIndexType to collection constructor instead - Remove lazy index infrastructure (LazyIndexWrapper, IndexProxy) - Simplify indexing.ts entry point to just export BasicIndex and BTreeIndex - Update all tests to explicitly set defaultIndexType where needed - Update changeset to reflect simplified approach Breaking changes: - createIndex() requires defaultIndexType on collection or indexType in config - enableIndexing()/enableBTreeIndexing() removed, use defaultIndexType instead
1 parent eb2f16e commit 6df29a2

21 files changed

+621
-861
lines changed

.changeset/optional-indexing.md

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,59 @@
22
"@tanstack/db": minor
33
---
44

5-
Make indexing optional with two index types for different use cases
5+
Make indexing explicit with two index types for different use cases
66

77
**Breaking Changes:**
88
- `autoIndex` now defaults to `off` instead of `eager`
99
- `BTreeIndex` is no longer exported from `@tanstack/db` main entry point
10+
- To use `createIndex()` or `autoIndex: 'eager'`, you must set `defaultIndexType` on the collection
1011

1112
**Changes:**
1213
- New `@tanstack/db/indexing` entry point for tree-shakeable indexing
13-
- **MapIndex** - Lightweight index for equality lookups (`eq`, `in`). Range queries (`gt`, `lt`, etc.) fall back to scanning.
14-
- **BTreeIndex** - Full-featured index with range queries and sorted iteration for ORDER BY optimization
15-
- `enableIndexing()` - Uses MapIndex (lightweight, for most use cases)
16-
- `enableBTreeIndexing()` - Uses BTreeIndex (for ORDER BY on large collections 10k+ items)
14+
- **BasicIndex** - Lightweight index using Map + sorted Array for both equality and range queries (`eq`, `in`, `gt`, `gte`, `lt`, `lte`). O(n) updates but fast reads.
15+
- **BTreeIndex** - Full-featured index with O(log n) updates and sorted iteration for ORDER BY optimization on large collections (10k+ items)
1716
- Dev mode suggestions (ON by default) warn when indexes would help
1817

1918
**Migration:**
2019

21-
If you were relying on auto-indexing, choose an approach based on your needs:
20+
If you were relying on auto-indexing, set `defaultIndexType` on your collections:
2221

23-
1. **Lightweight indexing** (equality lookups, range queries fall back to scan):
22+
1. **Lightweight indexing** (good for most use cases):
2423
```ts
25-
import { enableIndexing } from '@tanstack/db/indexing'
26-
enableIndexing() // Uses MapIndex - supports eq, in
24+
import { BasicIndex } from '@tanstack/db/indexing'
25+
26+
const collection = createCollection({
27+
defaultIndexType: BasicIndex,
28+
autoIndex: 'eager',
29+
// ...
30+
})
2731
```
2832

2933
2. **Full BTree indexing** (for ORDER BY optimization on large collections):
3034
```ts
31-
import { enableBTreeIndexing } from '@tanstack/db/indexing'
32-
enableBTreeIndexing() // Uses BTreeIndex - supports sorted iteration
35+
import { BTreeIndex } from '@tanstack/db/indexing'
36+
37+
const collection = createCollection({
38+
defaultIndexType: BTreeIndex,
39+
autoIndex: 'eager',
40+
// ...
41+
})
3342
```
3443

35-
3. **Per-collection explicit indexes** (best tree-shaking):
44+
3. **Per-index explicit type** (mix index types):
3645
```ts
37-
import { BTreeIndex } from '@tanstack/db/indexing'
38-
collection.createIndex((row) => row.userId, { indexType: BTreeIndex })
46+
import { BasicIndex, BTreeIndex } from '@tanstack/db/indexing'
47+
48+
const collection = createCollection({
49+
defaultIndexType: BasicIndex, // Default for createIndex()
50+
// ...
51+
})
52+
53+
// Override for specific indexes
54+
collection.createIndex((row) => row.date, { indexType: BTreeIndex })
3955
```
4056

4157
**Bundle Size Impact:**
4258
- No indexing: ~30% smaller bundle
43-
- MapIndex: ~5 KB (~1.3 KB gzipped)
59+
- BasicIndex: ~5 KB (~1.3 KB gzipped)
4460
- BTreeIndex: ~33 KB (~7.8 KB gzipped)

packages/db/src/collection/index.ts

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { CollectionMutationsManager } from "./mutations"
1313
import { CollectionEventsManager } from "./events.js"
1414
import type { CollectionSubscription } from "./subscription"
1515
import type { AllCollectionEvents, CollectionEventHandler } from "./events.js"
16-
import type { BaseIndex, IndexResolver } from "../indexes/base-index.js"
16+
import type { BaseIndex, IndexConstructor } from "../indexes/base-index.js"
1717
import type { IndexOptions } from "../indexes/index-options.js"
1818
import type {
1919
ChangeMessage,
@@ -35,8 +35,6 @@ import type {
3535
} from "../types"
3636
import type { SingleRowRefProxy } from "../query/builder/ref-proxy"
3737
import type { StandardSchemaV1 } from "@standard-schema/spec"
38-
import type { BTreeIndex } from "../indexes/btree-index.js"
39-
import type { IndexProxy } from "../indexes/lazy-index.js"
4038

4139
/**
4240
* Enhanced Collection interface that includes both data type T and utilities TUtils
@@ -347,6 +345,7 @@ export class CollectionImpl<
347345
this._indexes.setDeps({
348346
state: this._state,
349347
lifecycle: this._lifecycle,
348+
defaultIndexType: config.defaultIndexType,
350349
})
351350
this._lifecycle.setDeps({
352351
changes: this._changes,
@@ -523,38 +522,27 @@ export class CollectionImpl<
523522
* Indexes significantly improve query performance by allowing constant time lookups
524523
* and logarithmic time range queries instead of full scans.
525524
*
526-
* @template TResolver - The type of the index resolver (constructor or async loader)
527525
* @param indexCallback - Function that extracts the indexed value from each item
528526
* @param config - Configuration including index type and type-specific options
529-
* @returns An index proxy that provides access to the index when ready
527+
* @returns The created index
530528
*
531529
* @example
532-
* // Create a default B+ tree index
533-
* const ageIndex = collection.createIndex((row) => row.age)
530+
* ```ts
531+
* import { BasicIndex } from '@tanstack/db/indexing'
534532
*
535-
* // Create a ordered index with custom options
533+
* // Create an index with explicit type
536534
* const ageIndex = collection.createIndex((row) => row.age, {
537-
* indexType: BTreeIndex,
538-
* options: {
539-
* compareFn: customComparator,
540-
* compareOptions: { direction: 'asc', nulls: 'first', stringSort: 'lexical' }
541-
* },
542-
* name: 'age_btree'
535+
* indexType: BasicIndex
543536
* })
544537
*
545-
* // Create an async-loaded index
546-
* const textIndex = collection.createIndex((row) => row.content, {
547-
* indexType: async () => {
548-
* const { FullTextIndex } = await import('./indexes/fulltext.js')
549-
* return FullTextIndex
550-
* },
551-
* options: { language: 'en' }
552-
* })
538+
* // Create an index with collection's default type
539+
* const nameIndex = collection.createIndex((row) => row.name)
540+
* ```
553541
*/
554-
public createIndex<TResolver extends IndexResolver<TKey> = typeof BTreeIndex>(
542+
public createIndex<TIndexType extends IndexConstructor<TKey>>(
555543
indexCallback: (row: SingleRowRefProxy<TOutput>) => any,
556-
config: IndexOptions<TResolver> = {}
557-
): IndexProxy<TKey> {
544+
config: IndexOptions<TIndexType> = {}
545+
): BaseIndex<TKey> {
558546
return this._indexes.createIndex(indexCallback, config)
559547
}
560548

Lines changed: 30 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { IndexProxy, LazyIndexWrapper } from "../indexes/lazy-index"
21
import {
32
createSingleRowRefProxy,
43
toExpression,
54
} from "../query/builder/ref-proxy"
6-
import { getDefaultIndexType } from "../indexes/index-registry"
75
import type { StandardSchemaV1 } from "@standard-schema/spec"
8-
import type { BaseIndex, IndexResolver } from "../indexes/base-index"
6+
import type { BaseIndex, IndexConstructor } from "../indexes/base-index"
97
import type { ChangeMessage } from "../types"
108
import type { IndexOptions } from "../indexes/index-options"
119
import type { SingleRowRefProxy } from "../query/builder/ref-proxy"
@@ -20,146 +18,79 @@ export class CollectionIndexesManager<
2018
> {
2119
private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
2220
private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>
21+
private defaultIndexType: IndexConstructor<TKey> | undefined
2322

24-
public lazyIndexes = new Map<number, LazyIndexWrapper<TKey>>()
25-
public resolvedIndexes = new Map<number, BaseIndex<TKey>>()
26-
public isIndexesResolved = false
23+
public indexes = new Map<number, BaseIndex<TKey>>()
2724
public indexCounter = 0
2825

2926
constructor() {}
3027

3128
setDeps(deps: {
3229
state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
3330
lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
31+
defaultIndexType?: IndexConstructor<TKey>
3432
}) {
3533
this.state = deps.state
3634
this.lifecycle = deps.lifecycle
35+
this.defaultIndexType = deps.defaultIndexType
3736
}
3837

3938
/**
4039
* Creates an index on a collection for faster queries.
4140
*
42-
* Note: If no indexType is specified, this will use the registered default
43-
* index type. If no default is registered, an error will be thrown.
44-
*
4541
* @example
4642
* ```ts
4743
* // With explicit index type (recommended for tree-shaking)
48-
* import { BTreeIndex } from '@tanstack/db/indexing'
49-
* collection.createIndex((row) => row.userId, { indexType: BTreeIndex })
44+
* import { BasicIndex } from '@tanstack/db/indexing'
45+
* collection.createIndex((row) => row.userId, { indexType: BasicIndex })
5046
*
51-
* // With registered default (requires registerDefaultIndexType)
47+
* // With collection's default index type
5248
* collection.createIndex((row) => row.userId)
5349
* ```
5450
*/
55-
public createIndex<TResolver extends IndexResolver<TKey>>(
51+
public createIndex<TIndexType extends IndexConstructor<TKey>>(
5652
indexCallback: (row: SingleRowRefProxy<TOutput>) => any,
57-
config: IndexOptions<TResolver> = {}
58-
): IndexProxy<TKey> {
53+
config: IndexOptions<TIndexType> = {}
54+
): BaseIndex<TKey> {
5955
this.lifecycle.validateCollectionUsable(`createIndex`)
6056

6157
const indexId = ++this.indexCounter
6258
const singleRowRefProxy = createSingleRowRefProxy<TOutput>()
6359
const indexExpression = indexCallback(singleRowRefProxy)
6460
const expression = toExpression(indexExpression)
6561

66-
// Use provided index type, or fall back to registered default
67-
let resolver: TResolver
68-
if (config.indexType) {
69-
resolver = config.indexType
70-
} else {
71-
const defaultType = getDefaultIndexType()
72-
if (!defaultType) {
73-
throw new Error(
74-
`No index type specified and no default index type registered. ` +
75-
`Either pass indexType in config, or register a default:\n` +
76-
` import { registerDefaultIndexType, BTreeIndex } from '@tanstack/db/indexing'\n` +
77-
` registerDefaultIndexType(BTreeIndex)`
78-
)
79-
}
80-
resolver = defaultType as unknown as TResolver
62+
// Use provided index type, or fall back to collection's default
63+
const IndexType = config.indexType ?? this.defaultIndexType
64+
if (!IndexType) {
65+
throw new Error(
66+
`No index type specified and no defaultIndexType set on collection. ` +
67+
`Either pass indexType in config, or set defaultIndexType on the collection:\n` +
68+
` import { BasicIndex } from '@tanstack/db/indexing'\n` +
69+
` createCollection({ defaultIndexType: BasicIndex, ... })`
70+
)
8171
}
8272

83-
// Create lazy wrapper
84-
const lazyIndex = new LazyIndexWrapper<TKey>(
73+
// Create index synchronously
74+
const index = new IndexType(
8575
indexId,
8676
expression,
8777
config.name,
88-
resolver,
89-
config.options,
90-
this.state.entries()
91-
)
92-
93-
this.lazyIndexes.set(indexId, lazyIndex)
94-
95-
// For synchronous index constructors (like BTreeIndex), resolve immediately
96-
if (typeof resolver === `function` && resolver.prototype) {
97-
try {
98-
const resolvedIndex = lazyIndex.getResolved()
99-
this.resolvedIndexes.set(indexId, resolvedIndex)
100-
} catch {
101-
// Fallback to async resolution if sync resolution fails
102-
this.resolveSingleIndex(indexId, lazyIndex).catch((error) => {
103-
console.warn(`Failed to resolve index:`, error)
104-
})
105-
}
106-
} else if (this.isIndexesResolved) {
107-
// Async loader but indexes are already resolved - resolve this one
108-
this.resolveSingleIndex(indexId, lazyIndex).catch((error) => {
109-
console.warn(`Failed to resolve single index:`, error)
110-
})
111-
}
112-
113-
return new IndexProxy(indexId, lazyIndex)
114-
}
115-
116-
/**
117-
* Resolve all lazy indexes (called when collection first syncs)
118-
*/
119-
public async resolveAllIndexes(): Promise<void> {
120-
if (this.isIndexesResolved) return
121-
122-
const resolutionPromises = Array.from(this.lazyIndexes.entries()).map(
123-
async ([indexId, lazyIndex]) => {
124-
const resolvedIndex = await lazyIndex.resolve()
125-
126-
// Build index with current data
127-
resolvedIndex.build(this.state.entries())
128-
129-
this.resolvedIndexes.set(indexId, resolvedIndex)
130-
return { indexId, resolvedIndex }
131-
}
78+
config.options
13279
)
13380

134-
await Promise.all(resolutionPromises)
135-
this.isIndexesResolved = true
136-
}
81+
// Build with current data
82+
index.build(this.state.entries())
13783

138-
/**
139-
* Resolve a single index immediately
140-
*/
141-
private async resolveSingleIndex(
142-
indexId: number,
143-
lazyIndex: LazyIndexWrapper<TKey>
144-
): Promise<BaseIndex<TKey>> {
145-
const resolvedIndex = await lazyIndex.resolve()
146-
resolvedIndex.build(this.state.entries())
147-
this.resolvedIndexes.set(indexId, resolvedIndex)
148-
return resolvedIndex
149-
}
84+
this.indexes.set(indexId, index)
15085

151-
/**
152-
* Get resolved indexes for query optimization
153-
*/
154-
get indexes(): Map<number, BaseIndex<TKey>> {
155-
return this.resolvedIndexes
86+
return index
15687
}
15788

15889
/**
15990
* Updates all indexes when the collection changes
16091
*/
16192
public updateIndexes(changes: Array<ChangeMessage<TOutput, TKey>>): void {
162-
for (const index of this.resolvedIndexes.values()) {
93+
for (const index of this.indexes.values()) {
16394
for (const change of changes) {
16495
switch (change.type) {
16596
case `insert`:
@@ -181,11 +112,9 @@ export class CollectionIndexesManager<
181112
}
182113

183114
/**
184-
* Clean up the collection by stopping sync and clearing data
185-
* This can be called manually or automatically by garbage collection
115+
* Clean up indexes
186116
*/
187117
public cleanup(): void {
188-
this.lazyIndexes.clear()
189-
this.resolvedIndexes.clear()
118+
this.indexes.clear()
190119
}
191120
}

packages/db/src/collection/lifecycle.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,6 @@ export class CollectionLifecycleManager<
106106
const previousStatus = this.status
107107
this.status = newStatus
108108

109-
// Resolve indexes when collection becomes ready
110-
if (newStatus === `ready` && !this.indexes.isIndexesResolved) {
111-
// Resolve indexes asynchronously without blocking
112-
this.indexes.resolveAllIndexes().catch((error) => {
113-
console.warn(
114-
`${this.config.id ? `[${this.config.id}] ` : ``}Failed to resolve indexes:`,
115-
error
116-
)
117-
})
118-
}
119-
120109
// Emit event
121110
this.events.emitStatusChange(newStatus, previousStatus)
122111
}

packages/db/src/index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,18 @@ export * from "./paced-mutations"
1717
export * from "./strategies/index.js"
1818

1919
// Index system exports - types only from main entry
20-
// For BTreeIndex and other index implementations, import from '@tanstack/db/indexing'
20+
// For BasicIndex, BTreeIndex and other index implementations, import from '@tanstack/db/indexing'
2121
export type {
2222
IndexInterface,
2323
IndexConstructor,
24-
IndexResolver,
2524
IndexStats,
2625
IndexOperation,
2726
} from "./indexes/base-index.js"
2827
export { BaseIndex } from "./indexes/base-index.js"
29-
export type { IndexProxy } from "./indexes/lazy-index.js"
3028
export { type IndexOptions } from "./indexes/index-options.js"
3129

32-
// Index registry - allows checking if indexing is available
30+
// Dev mode utilities
3331
export {
34-
registerDefaultIndexType,
35-
isIndexingAvailable,
3632
configureIndexDevMode,
3733
isDevModeEnabled,
3834
} from "./indexes/index-registry.js"

0 commit comments

Comments
 (0)