@tanstack/db 0.0.26 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/dist/cjs/change-events.cjs +141 -0
  2. package/dist/cjs/change-events.cjs.map +1 -0
  3. package/dist/cjs/change-events.d.cts +49 -0
  4. package/dist/cjs/collection.cjs +236 -90
  5. package/dist/cjs/collection.cjs.map +1 -1
  6. package/dist/cjs/collection.d.cts +95 -20
  7. package/dist/cjs/errors.cjs +509 -1
  8. package/dist/cjs/errors.cjs.map +1 -1
  9. package/dist/cjs/errors.d.cts +225 -1
  10. package/dist/cjs/index.cjs +82 -3
  11. package/dist/cjs/index.cjs.map +1 -1
  12. package/dist/cjs/index.d.cts +5 -1
  13. package/dist/cjs/indexes/auto-index.cjs +64 -0
  14. package/dist/cjs/indexes/auto-index.cjs.map +1 -0
  15. package/dist/cjs/indexes/auto-index.d.cts +9 -0
  16. package/dist/cjs/indexes/base-index.cjs +46 -0
  17. package/dist/cjs/indexes/base-index.cjs.map +1 -0
  18. package/dist/cjs/indexes/base-index.d.cts +54 -0
  19. package/dist/cjs/indexes/index-options.d.cts +13 -0
  20. package/dist/cjs/indexes/lazy-index.cjs +193 -0
  21. package/dist/cjs/indexes/lazy-index.cjs.map +1 -0
  22. package/dist/cjs/indexes/lazy-index.d.cts +96 -0
  23. package/dist/cjs/indexes/ordered-index.cjs +227 -0
  24. package/dist/cjs/indexes/ordered-index.cjs.map +1 -0
  25. package/dist/cjs/indexes/ordered-index.d.cts +72 -0
  26. package/dist/cjs/local-storage.cjs +9 -15
  27. package/dist/cjs/local-storage.cjs.map +1 -1
  28. package/dist/cjs/query/builder/functions.cjs +11 -0
  29. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  30. package/dist/cjs/query/builder/functions.d.cts +4 -0
  31. package/dist/cjs/query/builder/index.cjs +6 -7
  32. package/dist/cjs/query/builder/index.cjs.map +1 -1
  33. package/dist/cjs/query/builder/ref-proxy.cjs +37 -0
  34. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  35. package/dist/cjs/query/builder/ref-proxy.d.cts +12 -0
  36. package/dist/cjs/query/compiler/evaluators.cjs +83 -58
  37. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  38. package/dist/cjs/query/compiler/evaluators.d.cts +8 -0
  39. package/dist/cjs/query/compiler/expressions.cjs +61 -0
  40. package/dist/cjs/query/compiler/expressions.cjs.map +1 -0
  41. package/dist/cjs/query/compiler/expressions.d.cts +25 -0
  42. package/dist/cjs/query/compiler/group-by.cjs +5 -10
  43. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  44. package/dist/cjs/query/compiler/index.cjs +23 -17
  45. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  46. package/dist/cjs/query/compiler/index.d.cts +12 -3
  47. package/dist/cjs/query/compiler/joins.cjs +61 -12
  48. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  49. package/dist/cjs/query/compiler/order-by.cjs +4 -34
  50. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  51. package/dist/cjs/query/compiler/types.d.cts +2 -2
  52. package/dist/cjs/query/live-query-collection.cjs +54 -12
  53. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  54. package/dist/cjs/query/optimizer.cjs +45 -7
  55. package/dist/cjs/query/optimizer.cjs.map +1 -1
  56. package/dist/cjs/query/optimizer.d.cts +13 -3
  57. package/dist/cjs/transactions.cjs +5 -4
  58. package/dist/cjs/transactions.cjs.map +1 -1
  59. package/dist/cjs/types.d.cts +31 -0
  60. package/dist/cjs/utils/array-utils.cjs +18 -0
  61. package/dist/cjs/utils/array-utils.cjs.map +1 -0
  62. package/dist/cjs/utils/array-utils.d.cts +8 -0
  63. package/dist/cjs/utils/comparison.cjs +52 -0
  64. package/dist/cjs/utils/comparison.cjs.map +1 -0
  65. package/dist/cjs/utils/comparison.d.cts +11 -0
  66. package/dist/cjs/utils/index-optimization.cjs +270 -0
  67. package/dist/cjs/utils/index-optimization.cjs.map +1 -0
  68. package/dist/cjs/utils/index-optimization.d.cts +29 -0
  69. package/dist/esm/change-events.d.ts +49 -0
  70. package/dist/esm/change-events.js +141 -0
  71. package/dist/esm/change-events.js.map +1 -0
  72. package/dist/esm/collection.d.ts +95 -20
  73. package/dist/esm/collection.js +234 -88
  74. package/dist/esm/collection.js.map +1 -1
  75. package/dist/esm/errors.d.ts +225 -1
  76. package/dist/esm/errors.js +510 -2
  77. package/dist/esm/errors.js.map +1 -1
  78. package/dist/esm/index.d.ts +5 -1
  79. package/dist/esm/index.js +81 -2
  80. package/dist/esm/index.js.map +1 -1
  81. package/dist/esm/indexes/auto-index.d.ts +9 -0
  82. package/dist/esm/indexes/auto-index.js +64 -0
  83. package/dist/esm/indexes/auto-index.js.map +1 -0
  84. package/dist/esm/indexes/base-index.d.ts +54 -0
  85. package/dist/esm/indexes/base-index.js +46 -0
  86. package/dist/esm/indexes/base-index.js.map +1 -0
  87. package/dist/esm/indexes/index-options.d.ts +13 -0
  88. package/dist/esm/indexes/lazy-index.d.ts +96 -0
  89. package/dist/esm/indexes/lazy-index.js +193 -0
  90. package/dist/esm/indexes/lazy-index.js.map +1 -0
  91. package/dist/esm/indexes/ordered-index.d.ts +72 -0
  92. package/dist/esm/indexes/ordered-index.js +227 -0
  93. package/dist/esm/indexes/ordered-index.js.map +1 -0
  94. package/dist/esm/local-storage.js +9 -15
  95. package/dist/esm/local-storage.js.map +1 -1
  96. package/dist/esm/query/builder/functions.d.ts +4 -0
  97. package/dist/esm/query/builder/functions.js +11 -0
  98. package/dist/esm/query/builder/functions.js.map +1 -1
  99. package/dist/esm/query/builder/index.js +6 -7
  100. package/dist/esm/query/builder/index.js.map +1 -1
  101. package/dist/esm/query/builder/ref-proxy.d.ts +12 -0
  102. package/dist/esm/query/builder/ref-proxy.js +37 -0
  103. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  104. package/dist/esm/query/compiler/evaluators.d.ts +8 -0
  105. package/dist/esm/query/compiler/evaluators.js +84 -59
  106. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  107. package/dist/esm/query/compiler/expressions.d.ts +25 -0
  108. package/dist/esm/query/compiler/expressions.js +61 -0
  109. package/dist/esm/query/compiler/expressions.js.map +1 -0
  110. package/dist/esm/query/compiler/group-by.js +5 -10
  111. package/dist/esm/query/compiler/group-by.js.map +1 -1
  112. package/dist/esm/query/compiler/index.d.ts +12 -3
  113. package/dist/esm/query/compiler/index.js +23 -17
  114. package/dist/esm/query/compiler/index.js.map +1 -1
  115. package/dist/esm/query/compiler/joins.js +61 -12
  116. package/dist/esm/query/compiler/joins.js.map +1 -1
  117. package/dist/esm/query/compiler/order-by.js +1 -31
  118. package/dist/esm/query/compiler/order-by.js.map +1 -1
  119. package/dist/esm/query/compiler/types.d.ts +2 -2
  120. package/dist/esm/query/live-query-collection.js +54 -12
  121. package/dist/esm/query/live-query-collection.js.map +1 -1
  122. package/dist/esm/query/optimizer.d.ts +13 -3
  123. package/dist/esm/query/optimizer.js +40 -2
  124. package/dist/esm/query/optimizer.js.map +1 -1
  125. package/dist/esm/transactions.js +5 -4
  126. package/dist/esm/transactions.js.map +1 -1
  127. package/dist/esm/types.d.ts +31 -0
  128. package/dist/esm/utils/array-utils.d.ts +8 -0
  129. package/dist/esm/utils/array-utils.js +18 -0
  130. package/dist/esm/utils/array-utils.js.map +1 -0
  131. package/dist/esm/utils/comparison.d.ts +11 -0
  132. package/dist/esm/utils/comparison.js +52 -0
  133. package/dist/esm/utils/comparison.js.map +1 -0
  134. package/dist/esm/utils/index-optimization.d.ts +29 -0
  135. package/dist/esm/utils/index-optimization.js +270 -0
  136. package/dist/esm/utils/index-optimization.js.map +1 -0
  137. package/package.json +3 -2
  138. package/src/change-events.ts +257 -0
  139. package/src/collection.ts +321 -110
  140. package/src/errors.ts +545 -1
  141. package/src/index.ts +7 -1
  142. package/src/indexes/auto-index.ts +108 -0
  143. package/src/indexes/base-index.ts +119 -0
  144. package/src/indexes/index-options.ts +42 -0
  145. package/src/indexes/lazy-index.ts +251 -0
  146. package/src/indexes/ordered-index.ts +305 -0
  147. package/src/local-storage.ts +16 -17
  148. package/src/query/builder/functions.ts +14 -0
  149. package/src/query/builder/index.ts +12 -7
  150. package/src/query/builder/ref-proxy.ts +65 -0
  151. package/src/query/compiler/evaluators.ts +114 -62
  152. package/src/query/compiler/expressions.ts +92 -0
  153. package/src/query/compiler/group-by.ts +10 -10
  154. package/src/query/compiler/index.ts +52 -22
  155. package/src/query/compiler/joins.ts +114 -15
  156. package/src/query/compiler/order-by.ts +1 -45
  157. package/src/query/compiler/types.ts +2 -2
  158. package/src/query/live-query-collection.ts +95 -15
  159. package/src/query/optimizer.ts +94 -5
  160. package/src/transactions.ts +10 -4
  161. package/src/types.ts +38 -0
  162. package/src/utils/array-utils.ts +28 -0
  163. package/src/utils/comparison.ts +79 -0
  164. package/src/utils/index-optimization.ts +546 -0
@@ -0,0 +1,119 @@
1
+ import { compileSingleRowExpression } from "../query/compiler/evaluators.js"
2
+ import { comparisonFunctions } from "../query/builder/functions.js"
3
+ import type { BasicExpression } from "../query/ir.js"
4
+
5
+ /**
6
+ * Operations that indexes can support, imported from available comparison functions
7
+ */
8
+ export const IndexOperation = comparisonFunctions
9
+
10
+ /**
11
+ * Type for index operation values
12
+ */
13
+ export type IndexOperation = (typeof comparisonFunctions)[number]
14
+
15
+ /**
16
+ * Statistics about index usage and performance
17
+ */
18
+ export interface IndexStats {
19
+ readonly entryCount: number
20
+ readonly lookupCount: number
21
+ readonly averageLookupTime: number
22
+ readonly lastUpdated: Date
23
+ }
24
+
25
+ /**
26
+ * Base abstract class that all index types extend
27
+ */
28
+ export abstract class BaseIndex<
29
+ TKey extends string | number = string | number,
30
+ > {
31
+ public readonly id: number
32
+ public readonly name?: string
33
+ public readonly expression: BasicExpression
34
+ public abstract readonly supportedOperations: Set<IndexOperation>
35
+
36
+ protected lookupCount = 0
37
+ protected totalLookupTime = 0
38
+ protected lastUpdated = new Date()
39
+
40
+ constructor(
41
+ id: number,
42
+ expression: BasicExpression,
43
+ name?: string,
44
+ options?: any
45
+ ) {
46
+ this.id = id
47
+ this.expression = expression
48
+ this.name = name
49
+ this.initialize(options)
50
+ }
51
+
52
+ // Abstract methods that each index type must implement
53
+ abstract add(key: TKey, item: any): void
54
+ abstract remove(key: TKey, item: any): void
55
+ abstract update(key: TKey, oldItem: any, newItem: any): void
56
+ abstract build(entries: Iterable<[TKey, any]>): void
57
+ abstract clear(): void
58
+ abstract lookup(operation: IndexOperation, value: any): Set<TKey>
59
+ abstract get keyCount(): number
60
+
61
+ // Common methods
62
+ supports(operation: IndexOperation): boolean {
63
+ return this.supportedOperations.has(operation)
64
+ }
65
+
66
+ matchesField(fieldPath: Array<string>): boolean {
67
+ return (
68
+ this.expression.type === `ref` &&
69
+ this.expression.path.length === fieldPath.length &&
70
+ this.expression.path.every((part, i) => part === fieldPath[i])
71
+ )
72
+ }
73
+
74
+ getStats(): IndexStats {
75
+ return {
76
+ entryCount: this.keyCount,
77
+ lookupCount: this.lookupCount,
78
+ averageLookupTime:
79
+ this.lookupCount > 0 ? this.totalLookupTime / this.lookupCount : 0,
80
+ lastUpdated: this.lastUpdated,
81
+ }
82
+ }
83
+
84
+ // Protected methods for subclasses
85
+ protected abstract initialize(options?: any): void
86
+
87
+ protected evaluateIndexExpression(item: any): any {
88
+ const evaluator = compileSingleRowExpression(this.expression)
89
+ return evaluator(item as Record<string, unknown>)
90
+ }
91
+
92
+ protected trackLookup(startTime: number): void {
93
+ const duration = performance.now() - startTime
94
+ this.lookupCount++
95
+ this.totalLookupTime += duration
96
+ }
97
+
98
+ protected updateTimestamp(): void {
99
+ this.lastUpdated = new Date()
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Type for index constructor
105
+ */
106
+ export type IndexConstructor<TKey extends string | number = string | number> =
107
+ new (
108
+ id: number,
109
+ expression: BasicExpression,
110
+ name?: string,
111
+ options?: any
112
+ ) => BaseIndex<TKey>
113
+
114
+ /**
115
+ * Index resolver can be either a class constructor or async loader
116
+ */
117
+ export type IndexResolver<TKey extends string | number = string | number> =
118
+ | IndexConstructor<TKey>
119
+ | (() => Promise<IndexConstructor<TKey>>)
@@ -0,0 +1,42 @@
1
+ import type { IndexConstructor, IndexResolver } from "./base-index.js"
2
+
3
+ /**
4
+ * Enhanced index options that support both sync and async resolvers
5
+ */
6
+ export interface IndexOptions<TResolver extends IndexResolver = IndexResolver> {
7
+ name?: string
8
+ indexType?: TResolver
9
+ options?: TResolver extends IndexConstructor<any>
10
+ ? TResolver extends new (
11
+ id: string,
12
+ expr: any,
13
+ name?: string,
14
+ options?: infer O
15
+ ) => any
16
+ ? O
17
+ : never
18
+ : TResolver extends () => Promise<infer TCtor>
19
+ ? TCtor extends new (
20
+ id: string,
21
+ expr: any,
22
+ name?: string,
23
+ options?: infer O
24
+ ) => any
25
+ ? O
26
+ : never
27
+ : never
28
+ }
29
+
30
+ /**
31
+ * Utility type to extract the constructed index type from a resolver
32
+ */
33
+ export type ResolvedIndexType<TResolver extends IndexResolver> =
34
+ TResolver extends IndexConstructor<any>
35
+ ? InstanceType<TResolver>
36
+ : TResolver extends () => Promise<IndexConstructor<any>>
37
+ ? TResolver extends () => Promise<infer TCtor>
38
+ ? TCtor extends IndexConstructor<any>
39
+ ? InstanceType<TCtor>
40
+ : never
41
+ : never
42
+ : never
@@ -0,0 +1,251 @@
1
+ import type {
2
+ BaseIndex,
3
+ IndexConstructor,
4
+ IndexResolver,
5
+ } from "./base-index.js"
6
+ import type { BasicExpression } from "../query/ir.js"
7
+
8
+ /**
9
+ * Utility to determine if a resolver is a constructor or async loader
10
+ */
11
+ function isConstructor<TKey extends string | number>(
12
+ resolver: IndexResolver<TKey>
13
+ ): resolver is IndexConstructor<TKey> {
14
+ // Check if it's a function with a prototype (constructor)
15
+ return (
16
+ typeof resolver === `function` &&
17
+ resolver.prototype !== undefined &&
18
+ resolver.prototype.constructor === resolver
19
+ )
20
+ }
21
+
22
+ /**
23
+ * Resolve index constructor from resolver
24
+ */
25
+ async function resolveIndexConstructor<TKey extends string | number>(
26
+ resolver: IndexResolver<TKey>
27
+ ): Promise<IndexConstructor<TKey>> {
28
+ if (isConstructor(resolver)) {
29
+ return resolver
30
+ } else {
31
+ // It's an async loader function
32
+ return await resolver()
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Wrapper that defers index creation until first sync
38
+ */
39
+ export class LazyIndexWrapper<TKey extends string | number = string | number> {
40
+ private indexPromise: Promise<BaseIndex<TKey>> | null = null
41
+ private resolvedIndex: BaseIndex<TKey> | null = null
42
+
43
+ constructor(
44
+ private id: number,
45
+ private expression: BasicExpression,
46
+ private name: string | undefined,
47
+ private resolver: IndexResolver<TKey>,
48
+ private options: any,
49
+ private collectionEntries?: Iterable<[TKey, any]>
50
+ ) {
51
+ // For synchronous constructors, resolve immediately
52
+ if (isConstructor(this.resolver)) {
53
+ this.resolvedIndex = new this.resolver(
54
+ this.id,
55
+ this.expression,
56
+ this.name,
57
+ this.options
58
+ )
59
+ // Build with initial data if provided
60
+ if (this.collectionEntries) {
61
+ this.resolvedIndex.build(this.collectionEntries)
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Resolve the actual index
68
+ */
69
+ async resolve(): Promise<BaseIndex<TKey>> {
70
+ if (this.resolvedIndex) {
71
+ return this.resolvedIndex
72
+ }
73
+
74
+ if (!this.indexPromise) {
75
+ this.indexPromise = this.createIndex()
76
+ }
77
+
78
+ this.resolvedIndex = await this.indexPromise
79
+ return this.resolvedIndex
80
+ }
81
+
82
+ /**
83
+ * Check if already resolved
84
+ */
85
+ isResolved(): boolean {
86
+ return this.resolvedIndex !== null
87
+ }
88
+
89
+ /**
90
+ * Get resolved index (throws if not ready)
91
+ */
92
+ getResolved(): BaseIndex<TKey> {
93
+ if (!this.resolvedIndex) {
94
+ throw new Error(
95
+ `Index ${this.id} has not been resolved yet. Ensure collection is synced.`
96
+ )
97
+ }
98
+ return this.resolvedIndex
99
+ }
100
+
101
+ /**
102
+ * Get the index ID
103
+ */
104
+ getId(): number {
105
+ return this.id
106
+ }
107
+
108
+ /**
109
+ * Get the index name
110
+ */
111
+ getName(): string | undefined {
112
+ return this.name
113
+ }
114
+
115
+ /**
116
+ * Get the index expression
117
+ */
118
+ getExpression(): BasicExpression {
119
+ return this.expression
120
+ }
121
+
122
+ private async createIndex(): Promise<BaseIndex<TKey>> {
123
+ const IndexClass = await resolveIndexConstructor(this.resolver)
124
+ return new IndexClass(this.id, this.expression, this.name, this.options)
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Proxy that provides synchronous interface while index loads asynchronously
130
+ */
131
+ export class IndexProxy<TKey extends string | number = string | number> {
132
+ constructor(
133
+ private indexId: number,
134
+ private lazyIndex: LazyIndexWrapper<TKey>
135
+ ) {}
136
+
137
+ /**
138
+ * Get the resolved index (throws if not ready)
139
+ */
140
+ get index(): BaseIndex<TKey> {
141
+ return this.lazyIndex.getResolved()
142
+ }
143
+
144
+ /**
145
+ * Check if index is ready
146
+ */
147
+ get isReady(): boolean {
148
+ return this.lazyIndex.isResolved()
149
+ }
150
+
151
+ /**
152
+ * Wait for index to be ready
153
+ */
154
+ async whenReady(): Promise<BaseIndex<TKey>> {
155
+ return await this.lazyIndex.resolve()
156
+ }
157
+
158
+ /**
159
+ * Get the index ID
160
+ */
161
+ get id(): number {
162
+ return this.indexId
163
+ }
164
+
165
+ /**
166
+ * Get the index name (throws if not ready)
167
+ */
168
+ get name(): string | undefined {
169
+ if (this.isReady) {
170
+ return this.index.name
171
+ }
172
+ return this.lazyIndex.getName()
173
+ }
174
+
175
+ /**
176
+ * Get the index expression (available immediately)
177
+ */
178
+ get expression(): BasicExpression {
179
+ return this.lazyIndex.getExpression()
180
+ }
181
+
182
+ /**
183
+ * Check if index supports an operation (throws if not ready)
184
+ */
185
+ supports(operation: any): boolean {
186
+ return this.index.supports(operation)
187
+ }
188
+
189
+ /**
190
+ * Get index statistics (throws if not ready)
191
+ */
192
+ getStats() {
193
+ return this.index.getStats()
194
+ }
195
+
196
+ /**
197
+ * Check if index matches a field path (available immediately)
198
+ */
199
+ matchesField(fieldPath: Array<string>): boolean {
200
+ const expr = this.expression
201
+ return (
202
+ expr.type === `ref` &&
203
+ expr.path.length === fieldPath.length &&
204
+ expr.path.every((part, i) => part === fieldPath[i])
205
+ )
206
+ }
207
+
208
+ /**
209
+ * Get the key count (throws if not ready)
210
+ */
211
+ get keyCount(): number {
212
+ return this.index.keyCount
213
+ }
214
+
215
+ // Test compatibility properties - delegate to resolved index
216
+ get indexedKeysSet(): Set<TKey> {
217
+ const resolved = this.index as any
218
+ return resolved.indexedKeysSet
219
+ }
220
+
221
+ get orderedEntriesArray(): Array<[any, Set<TKey>]> {
222
+ const resolved = this.index as any
223
+ return resolved.orderedEntriesArray
224
+ }
225
+
226
+ get valueMapData(): Map<any, Set<TKey>> {
227
+ const resolved = this.index as any
228
+ return resolved.valueMapData
229
+ }
230
+
231
+ // BTreeIndex compatibility methods
232
+ equalityLookup(value: any): Set<TKey> {
233
+ const resolved = this.index as any
234
+ return resolved.equalityLookup?.(value) ?? new Set()
235
+ }
236
+
237
+ rangeQuery(options: any): Set<TKey> {
238
+ const resolved = this.index as any
239
+ return resolved.rangeQuery?.(options) ?? new Set()
240
+ }
241
+
242
+ inArrayLookup(values: Array<any>): Set<TKey> {
243
+ const resolved = this.index as any
244
+ return resolved.inArrayLookup?.(values) ?? new Set()
245
+ }
246
+
247
+ // Internal method for the collection to get the lazy wrapper
248
+ _getLazyWrapper(): LazyIndexWrapper<TKey> {
249
+ return this.lazyIndex
250
+ }
251
+ }
@@ -0,0 +1,305 @@
1
+ import { ascComparator } from "../utils/comparison.js"
2
+ import { findInsertPosition } from "../utils/array-utils.js"
3
+ import { BaseIndex } from "./base-index.js"
4
+ import type { IndexOperation } from "./base-index.js"
5
+
6
+ /**
7
+ * Options for Ordered index
8
+ */
9
+ export interface OrderedIndexOptions {
10
+ compareFn?: (a: any, b: any) => number
11
+ }
12
+
13
+ /**
14
+ * Options for range queries
15
+ */
16
+ export interface RangeQueryOptions {
17
+ from?: any
18
+ to?: any
19
+ fromInclusive?: boolean
20
+ toInclusive?: boolean
21
+ }
22
+
23
+ /**
24
+ * Ordered index for sorted data with range queries
25
+ * This maintains items in sorted order and provides efficient range operations
26
+ */
27
+ export class OrderedIndex<
28
+ TKey extends string | number = string | number,
29
+ > extends BaseIndex<TKey> {
30
+ public readonly supportedOperations = new Set<IndexOperation>([
31
+ `eq`,
32
+ `gt`,
33
+ `gte`,
34
+ `lt`,
35
+ `lte`,
36
+ `in`,
37
+ ])
38
+
39
+ // Internal data structures - private to hide implementation details
40
+ private orderedEntries: Array<[any, Set<TKey>]> = []
41
+ private valueMap = new Map<any, Set<TKey>>()
42
+ private indexedKeys = new Set<TKey>()
43
+ private compareFn: (a: any, b: any) => number = ascComparator
44
+
45
+ protected initialize(options?: OrderedIndexOptions): void {
46
+ this.compareFn = options?.compareFn ?? ascComparator
47
+ }
48
+
49
+ /**
50
+ * Adds a value to the index
51
+ */
52
+ add(key: TKey, item: any): void {
53
+ let indexedValue: any
54
+ try {
55
+ indexedValue = this.evaluateIndexExpression(item)
56
+ } catch (error) {
57
+ throw new Error(
58
+ `Failed to evaluate index expression for key ${key}: ${error}`
59
+ )
60
+ }
61
+
62
+ // Check if this value already exists
63
+ if (this.valueMap.has(indexedValue)) {
64
+ // Add to existing set
65
+ this.valueMap.get(indexedValue)!.add(key)
66
+ } else {
67
+ // Create new set for this value
68
+ const keySet = new Set<TKey>([key])
69
+ this.valueMap.set(indexedValue, keySet)
70
+
71
+ // Find correct position in ordered entries using binary search
72
+ const insertIndex = findInsertPosition(
73
+ this.orderedEntries,
74
+ indexedValue,
75
+ this.compareFn
76
+ )
77
+ this.orderedEntries.splice(insertIndex, 0, [indexedValue, keySet])
78
+ }
79
+
80
+ this.indexedKeys.add(key)
81
+ this.updateTimestamp()
82
+ }
83
+
84
+ /**
85
+ * Removes a value from the index
86
+ */
87
+ remove(key: TKey, item: any): void {
88
+ let indexedValue: any
89
+ try {
90
+ indexedValue = this.evaluateIndexExpression(item)
91
+ } catch (error) {
92
+ console.warn(
93
+ `Failed to evaluate index expression for key ${key} during removal:`,
94
+ error
95
+ )
96
+ return
97
+ }
98
+
99
+ if (this.valueMap.has(indexedValue)) {
100
+ const keySet = this.valueMap.get(indexedValue)!
101
+ keySet.delete(key)
102
+
103
+ // If set is now empty, remove the entry entirely
104
+ if (keySet.size === 0) {
105
+ this.valueMap.delete(indexedValue)
106
+
107
+ // Find and remove from ordered entries
108
+ const index = this.orderedEntries.findIndex(
109
+ ([value]) => this.compareFn(value, indexedValue) === 0
110
+ )
111
+ if (index !== -1) {
112
+ this.orderedEntries.splice(index, 1)
113
+ }
114
+ }
115
+ }
116
+
117
+ this.indexedKeys.delete(key)
118
+ this.updateTimestamp()
119
+ }
120
+
121
+ /**
122
+ * Updates a value in the index
123
+ */
124
+ update(key: TKey, oldItem: any, newItem: any): void {
125
+ this.remove(key, oldItem)
126
+ this.add(key, newItem)
127
+ }
128
+
129
+ /**
130
+ * Builds the index from a collection of entries
131
+ */
132
+ build(entries: Iterable<[TKey, any]>): void {
133
+ this.clear()
134
+
135
+ for (const [key, item] of entries) {
136
+ this.add(key, item)
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Clears all data from the index
142
+ */
143
+ clear(): void {
144
+ this.orderedEntries = []
145
+ this.valueMap.clear()
146
+ this.indexedKeys.clear()
147
+ this.updateTimestamp()
148
+ }
149
+
150
+ /**
151
+ * Performs a lookup operation
152
+ */
153
+ lookup(operation: IndexOperation, value: any): Set<TKey> {
154
+ const startTime = performance.now()
155
+
156
+ let result: Set<TKey>
157
+
158
+ switch (operation) {
159
+ case `eq`:
160
+ result = this.equalityLookup(value)
161
+ break
162
+ case `gt`:
163
+ result = this.rangeQuery({ from: value, fromInclusive: false })
164
+ break
165
+ case `gte`:
166
+ result = this.rangeQuery({ from: value, fromInclusive: true })
167
+ break
168
+ case `lt`:
169
+ result = this.rangeQuery({ to: value, toInclusive: false })
170
+ break
171
+ case `lte`:
172
+ result = this.rangeQuery({ to: value, toInclusive: true })
173
+ break
174
+ case `in`:
175
+ result = this.inArrayLookup(value)
176
+ break
177
+ default:
178
+ throw new Error(`Operation ${operation} not supported by OrderedIndex`)
179
+ }
180
+
181
+ this.trackLookup(startTime)
182
+ return result
183
+ }
184
+
185
+ /**
186
+ * Gets the number of indexed keys
187
+ */
188
+ get keyCount(): number {
189
+ return this.indexedKeys.size
190
+ }
191
+
192
+ // Public methods for backward compatibility (used by tests)
193
+
194
+ /**
195
+ * Performs an equality lookup
196
+ */
197
+ equalityLookup(value: any): Set<TKey> {
198
+ return new Set(this.valueMap.get(value) ?? [])
199
+ }
200
+
201
+ /**
202
+ * Performs a range query with options
203
+ * This is more efficient for compound queries like "WHERE a > 5 AND a < 10"
204
+ */
205
+ rangeQuery(options: RangeQueryOptions = {}): Set<TKey> {
206
+ const { from, to, fromInclusive = true, toInclusive = true } = options
207
+ const result = new Set<TKey>()
208
+
209
+ if (this.orderedEntries.length === 0) {
210
+ return result
211
+ }
212
+
213
+ // Find start position
214
+ let startIndex = 0
215
+ if (from !== undefined) {
216
+ const fromInsertIndex = findInsertPosition(
217
+ this.orderedEntries,
218
+ from,
219
+ this.compareFn
220
+ )
221
+
222
+ if (fromInclusive) {
223
+ // Include values equal to 'from'
224
+ startIndex = fromInsertIndex
225
+ } else {
226
+ // Exclude values equal to 'from'
227
+ startIndex = fromInsertIndex
228
+ // Skip the value if it exists at this position
229
+ if (
230
+ startIndex < this.orderedEntries.length &&
231
+ this.compareFn(this.orderedEntries[startIndex]![0], from) === 0
232
+ ) {
233
+ startIndex++
234
+ }
235
+ }
236
+ }
237
+
238
+ // Find end position
239
+ let endIndex = this.orderedEntries.length
240
+ if (to !== undefined) {
241
+ const toInsertIndex = findInsertPosition(
242
+ this.orderedEntries,
243
+ to,
244
+ this.compareFn
245
+ )
246
+
247
+ if (toInclusive) {
248
+ // Include values equal to 'to'
249
+ endIndex = toInsertIndex
250
+ // Include the value if it exists at this position
251
+ if (
252
+ toInsertIndex < this.orderedEntries.length &&
253
+ this.compareFn(this.orderedEntries[toInsertIndex]![0], to) === 0
254
+ ) {
255
+ endIndex = toInsertIndex + 1
256
+ }
257
+ } else {
258
+ // Exclude values equal to 'to'
259
+ endIndex = toInsertIndex
260
+ }
261
+ }
262
+
263
+ // Ensure startIndex doesn't exceed endIndex
264
+ if (startIndex >= endIndex) {
265
+ return result
266
+ }
267
+
268
+ // Collect keys from the range
269
+ for (let i = startIndex; i < endIndex; i++) {
270
+ const keys = this.orderedEntries[i]![1]
271
+ keys.forEach((key) => result.add(key))
272
+ }
273
+
274
+ return result
275
+ }
276
+
277
+ /**
278
+ * Performs an IN array lookup
279
+ */
280
+ inArrayLookup(values: Array<any>): Set<TKey> {
281
+ const result = new Set<TKey>()
282
+
283
+ for (const value of values) {
284
+ const keys = this.valueMap.get(value)
285
+ if (keys) {
286
+ keys.forEach((key) => result.add(key))
287
+ }
288
+ }
289
+
290
+ return result
291
+ }
292
+
293
+ // Getter methods for testing compatibility
294
+ get indexedKeysSet(): Set<TKey> {
295
+ return this.indexedKeys
296
+ }
297
+
298
+ get orderedEntriesArray(): Array<[any, Set<TKey>]> {
299
+ return this.orderedEntries
300
+ }
301
+
302
+ get valueMapData(): Map<any, Set<TKey>> {
303
+ return this.valueMap
304
+ }
305
+ }