@tanstack/db 0.5.33 → 0.6.0

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 (273) hide show
  1. package/dist/cjs/collection/change-events.cjs.map +1 -1
  2. package/dist/cjs/collection/change-events.d.cts +3 -2
  3. package/dist/cjs/collection/changes.cjs +13 -4
  4. package/dist/cjs/collection/changes.cjs.map +1 -1
  5. package/dist/cjs/collection/changes.d.cts +10 -1
  6. package/dist/cjs/collection/cleanup-queue.cjs +89 -0
  7. package/dist/cjs/collection/cleanup-queue.cjs.map +1 -0
  8. package/dist/cjs/collection/cleanup-queue.d.cts +30 -0
  9. package/dist/cjs/collection/events.cjs +14 -0
  10. package/dist/cjs/collection/events.cjs.map +1 -1
  11. package/dist/cjs/collection/events.d.cts +39 -1
  12. package/dist/cjs/collection/index.cjs +66 -28
  13. package/dist/cjs/collection/index.cjs.map +1 -1
  14. package/dist/cjs/collection/index.d.cts +49 -36
  15. package/dist/cjs/collection/indexes.cjs +211 -62
  16. package/dist/cjs/collection/indexes.cjs.map +1 -1
  17. package/dist/cjs/collection/indexes.d.cts +27 -17
  18. package/dist/cjs/collection/lifecycle.cjs +5 -22
  19. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  20. package/dist/cjs/collection/lifecycle.d.cts +0 -1
  21. package/dist/cjs/collection/mutations.cjs +18 -0
  22. package/dist/cjs/collection/mutations.cjs.map +1 -1
  23. package/dist/cjs/collection/mutations.d.cts +1 -0
  24. package/dist/cjs/collection/state.cjs +381 -53
  25. package/dist/cjs/collection/state.cjs.map +1 -1
  26. package/dist/cjs/collection/state.d.cts +65 -1
  27. package/dist/cjs/collection/subscription.cjs +6 -0
  28. package/dist/cjs/collection/subscription.cjs.map +1 -1
  29. package/dist/cjs/collection/subscription.d.cts +4 -0
  30. package/dist/cjs/collection/sync.cjs +108 -1
  31. package/dist/cjs/collection/sync.cjs.map +1 -1
  32. package/dist/cjs/collection/sync.d.cts +2 -0
  33. package/dist/cjs/collection/transaction-metadata.cjs +5 -0
  34. package/dist/cjs/collection/transaction-metadata.cjs.map +1 -0
  35. package/dist/cjs/collection/transaction-metadata.d.cts +1 -0
  36. package/dist/cjs/errors.cjs +8 -0
  37. package/dist/cjs/errors.cjs.map +1 -1
  38. package/dist/cjs/errors.d.cts +3 -0
  39. package/dist/cjs/index.cjs +22 -4
  40. package/dist/cjs/index.cjs.map +1 -1
  41. package/dist/cjs/index.d.cts +11 -3
  42. package/dist/cjs/indexes/auto-index.cjs +13 -6
  43. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  44. package/dist/cjs/indexes/base-index.cjs +0 -3
  45. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  46. package/dist/cjs/indexes/base-index.d.cts +2 -6
  47. package/dist/cjs/indexes/basic-index.cjs +361 -0
  48. package/dist/cjs/indexes/basic-index.cjs.map +1 -0
  49. package/dist/cjs/indexes/basic-index.d.cts +102 -0
  50. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  51. package/dist/cjs/indexes/btree-index.d.cts +1 -1
  52. package/dist/cjs/indexes/index-options.d.cts +8 -9
  53. package/dist/cjs/indexes/index-registry.cjs +89 -0
  54. package/dist/cjs/indexes/index-registry.cjs.map +1 -0
  55. package/dist/cjs/indexes/index-registry.d.cts +61 -0
  56. package/dist/cjs/local-only.cjs +5 -0
  57. package/dist/cjs/local-only.cjs.map +1 -1
  58. package/dist/cjs/query/builder/functions.cjs +27 -11
  59. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  60. package/dist/cjs/query/builder/functions.d.cts +25 -3
  61. package/dist/cjs/query/builder/index.cjs +200 -39
  62. package/dist/cjs/query/builder/index.cjs.map +1 -1
  63. package/dist/cjs/query/builder/index.d.cts +4 -3
  64. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  65. package/dist/cjs/query/builder/ref-proxy.d.cts +14 -3
  66. package/dist/cjs/query/builder/types.d.cts +84 -19
  67. package/dist/cjs/query/compiler/evaluators.cjs +51 -0
  68. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  69. package/dist/cjs/query/compiler/group-by.cjs +100 -28
  70. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  71. package/dist/cjs/query/compiler/group-by.d.cts +4 -2
  72. package/dist/cjs/query/compiler/index.cjs +283 -11
  73. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  74. package/dist/cjs/query/compiler/index.d.cts +30 -2
  75. package/dist/cjs/query/compiler/order-by.cjs +29 -10
  76. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  77. package/dist/cjs/query/compiler/order-by.d.cts +1 -1
  78. package/dist/cjs/query/compiler/select.cjs +8 -0
  79. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  80. package/dist/cjs/query/index.d.cts +2 -1
  81. package/dist/cjs/query/ir.cjs +18 -1
  82. package/dist/cjs/query/ir.cjs.map +1 -1
  83. package/dist/cjs/query/ir.d.cts +21 -1
  84. package/dist/cjs/query/live/collection-config-builder.cjs +501 -5
  85. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  86. package/dist/cjs/query/live/collection-config-builder.d.cts +7 -0
  87. package/dist/cjs/query/live/types.d.cts +3 -3
  88. package/dist/cjs/query/live/utils.cjs +43 -3
  89. package/dist/cjs/query/live/utils.cjs.map +1 -1
  90. package/dist/cjs/query/live/utils.d.cts +1 -0
  91. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  92. package/dist/cjs/query/live-query-collection.d.cts +9 -6
  93. package/dist/cjs/query/query-once.cjs.map +1 -1
  94. package/dist/cjs/query/query-once.d.cts +7 -5
  95. package/dist/cjs/query/subset-dedupe.cjs +9 -3
  96. package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
  97. package/dist/cjs/types.d.cts +42 -8
  98. package/dist/cjs/utils/array-utils.cjs +27 -0
  99. package/dist/cjs/utils/array-utils.cjs.map +1 -0
  100. package/dist/cjs/utils/array-utils.d.cts +16 -0
  101. package/dist/cjs/utils/comparison.cjs +11 -0
  102. package/dist/cjs/utils/comparison.cjs.map +1 -1
  103. package/dist/cjs/utils/index-optimization.cjs +4 -0
  104. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  105. package/dist/cjs/utils.cjs +7 -9
  106. package/dist/cjs/utils.cjs.map +1 -1
  107. package/dist/cjs/utils.d.cts +6 -1
  108. package/dist/cjs/virtual-props.cjs +33 -0
  109. package/dist/cjs/virtual-props.cjs.map +1 -0
  110. package/dist/cjs/virtual-props.d.cts +196 -0
  111. package/dist/esm/collection/change-events.d.ts +3 -2
  112. package/dist/esm/collection/change-events.js.map +1 -1
  113. package/dist/esm/collection/changes.d.ts +10 -1
  114. package/dist/esm/collection/changes.js +13 -4
  115. package/dist/esm/collection/changes.js.map +1 -1
  116. package/dist/esm/collection/cleanup-queue.d.ts +30 -0
  117. package/dist/esm/collection/cleanup-queue.js +89 -0
  118. package/dist/esm/collection/cleanup-queue.js.map +1 -0
  119. package/dist/esm/collection/events.d.ts +39 -1
  120. package/dist/esm/collection/events.js +14 -0
  121. package/dist/esm/collection/events.js.map +1 -1
  122. package/dist/esm/collection/index.d.ts +49 -36
  123. package/dist/esm/collection/index.js +67 -29
  124. package/dist/esm/collection/index.js.map +1 -1
  125. package/dist/esm/collection/indexes.d.ts +27 -17
  126. package/dist/esm/collection/indexes.js +211 -62
  127. package/dist/esm/collection/indexes.js.map +1 -1
  128. package/dist/esm/collection/lifecycle.d.ts +0 -1
  129. package/dist/esm/collection/lifecycle.js +5 -22
  130. package/dist/esm/collection/lifecycle.js.map +1 -1
  131. package/dist/esm/collection/mutations.d.ts +1 -0
  132. package/dist/esm/collection/mutations.js +18 -0
  133. package/dist/esm/collection/mutations.js.map +1 -1
  134. package/dist/esm/collection/state.d.ts +65 -1
  135. package/dist/esm/collection/state.js +381 -53
  136. package/dist/esm/collection/state.js.map +1 -1
  137. package/dist/esm/collection/subscription.d.ts +4 -0
  138. package/dist/esm/collection/subscription.js +6 -0
  139. package/dist/esm/collection/subscription.js.map +1 -1
  140. package/dist/esm/collection/sync.d.ts +2 -0
  141. package/dist/esm/collection/sync.js +108 -1
  142. package/dist/esm/collection/sync.js.map +1 -1
  143. package/dist/esm/collection/transaction-metadata.d.ts +1 -0
  144. package/dist/esm/collection/transaction-metadata.js +5 -0
  145. package/dist/esm/collection/transaction-metadata.js.map +1 -0
  146. package/dist/esm/errors.d.ts +3 -0
  147. package/dist/esm/errors.js +8 -0
  148. package/dist/esm/errors.js.map +1 -1
  149. package/dist/esm/index.d.ts +11 -3
  150. package/dist/esm/index.js +25 -7
  151. package/dist/esm/index.js.map +1 -1
  152. package/dist/esm/indexes/auto-index.js +13 -6
  153. package/dist/esm/indexes/auto-index.js.map +1 -1
  154. package/dist/esm/indexes/base-index.d.ts +2 -6
  155. package/dist/esm/indexes/base-index.js +1 -4
  156. package/dist/esm/indexes/base-index.js.map +1 -1
  157. package/dist/esm/indexes/basic-index.d.ts +102 -0
  158. package/dist/esm/indexes/basic-index.js +361 -0
  159. package/dist/esm/indexes/basic-index.js.map +1 -0
  160. package/dist/esm/indexes/btree-index.d.ts +1 -1
  161. package/dist/esm/indexes/btree-index.js.map +1 -1
  162. package/dist/esm/indexes/index-options.d.ts +8 -9
  163. package/dist/esm/indexes/index-registry.d.ts +61 -0
  164. package/dist/esm/indexes/index-registry.js +89 -0
  165. package/dist/esm/indexes/index-registry.js.map +1 -0
  166. package/dist/esm/local-only.js +5 -0
  167. package/dist/esm/local-only.js.map +1 -1
  168. package/dist/esm/query/builder/functions.d.ts +25 -3
  169. package/dist/esm/query/builder/functions.js +27 -11
  170. package/dist/esm/query/builder/functions.js.map +1 -1
  171. package/dist/esm/query/builder/index.d.ts +4 -3
  172. package/dist/esm/query/builder/index.js +201 -40
  173. package/dist/esm/query/builder/index.js.map +1 -1
  174. package/dist/esm/query/builder/ref-proxy.d.ts +14 -3
  175. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  176. package/dist/esm/query/builder/types.d.ts +84 -19
  177. package/dist/esm/query/compiler/evaluators.js +51 -0
  178. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  179. package/dist/esm/query/compiler/group-by.d.ts +4 -2
  180. package/dist/esm/query/compiler/group-by.js +101 -29
  181. package/dist/esm/query/compiler/group-by.js.map +1 -1
  182. package/dist/esm/query/compiler/index.d.ts +30 -2
  183. package/dist/esm/query/compiler/index.js +285 -13
  184. package/dist/esm/query/compiler/index.js.map +1 -1
  185. package/dist/esm/query/compiler/order-by.d.ts +1 -1
  186. package/dist/esm/query/compiler/order-by.js +30 -11
  187. package/dist/esm/query/compiler/order-by.js.map +1 -1
  188. package/dist/esm/query/compiler/select.js +8 -0
  189. package/dist/esm/query/compiler/select.js.map +1 -1
  190. package/dist/esm/query/index.d.ts +2 -1
  191. package/dist/esm/query/ir.d.ts +21 -1
  192. package/dist/esm/query/ir.js +18 -1
  193. package/dist/esm/query/ir.js.map +1 -1
  194. package/dist/esm/query/live/collection-config-builder.d.ts +7 -0
  195. package/dist/esm/query/live/collection-config-builder.js +503 -7
  196. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  197. package/dist/esm/query/live/types.d.ts +3 -3
  198. package/dist/esm/query/live/utils.d.ts +1 -0
  199. package/dist/esm/query/live/utils.js +43 -3
  200. package/dist/esm/query/live/utils.js.map +1 -1
  201. package/dist/esm/query/live-query-collection.d.ts +9 -6
  202. package/dist/esm/query/live-query-collection.js.map +1 -1
  203. package/dist/esm/query/query-once.d.ts +7 -5
  204. package/dist/esm/query/query-once.js.map +1 -1
  205. package/dist/esm/query/subset-dedupe.js +9 -3
  206. package/dist/esm/query/subset-dedupe.js.map +1 -1
  207. package/dist/esm/types.d.ts +42 -8
  208. package/dist/esm/utils/array-utils.d.ts +16 -0
  209. package/dist/esm/utils/array-utils.js +27 -0
  210. package/dist/esm/utils/array-utils.js.map +1 -0
  211. package/dist/esm/utils/comparison.js +11 -0
  212. package/dist/esm/utils/comparison.js.map +1 -1
  213. package/dist/esm/utils/index-optimization.js +4 -0
  214. package/dist/esm/utils/index-optimization.js.map +1 -1
  215. package/dist/esm/utils.d.ts +6 -1
  216. package/dist/esm/utils.js +7 -9
  217. package/dist/esm/utils.js.map +1 -1
  218. package/dist/esm/virtual-props.d.ts +196 -0
  219. package/dist/esm/virtual-props.js +33 -0
  220. package/dist/esm/virtual-props.js.map +1 -0
  221. package/package.json +2 -2
  222. package/skills/db-core/collection-setup/references/electric-adapter.md +1 -1
  223. package/src/collection/change-events.ts +13 -9
  224. package/src/collection/changes.ts +30 -7
  225. package/src/collection/cleanup-queue.ts +105 -0
  226. package/src/collection/events.ts +65 -0
  227. package/src/collection/index.ts +110 -45
  228. package/src/collection/indexes.ts +283 -76
  229. package/src/collection/lifecycle.ts +5 -26
  230. package/src/collection/mutations.ts +21 -0
  231. package/src/collection/state.ts +545 -71
  232. package/src/collection/subscription.ts +7 -0
  233. package/src/collection/sync.ts +137 -0
  234. package/src/collection/transaction-metadata.ts +1 -0
  235. package/src/errors.ts +9 -0
  236. package/src/index.ts +46 -3
  237. package/src/indexes/auto-index.ts +18 -8
  238. package/src/indexes/base-index.ts +2 -10
  239. package/src/indexes/basic-index.ts +507 -0
  240. package/src/indexes/btree-index.ts +1 -1
  241. package/src/indexes/index-options.ts +17 -37
  242. package/src/indexes/index-registry.ts +174 -0
  243. package/src/local-only.ts +7 -0
  244. package/src/query/builder/functions.ts +84 -7
  245. package/src/query/builder/index.ts +329 -9
  246. package/src/query/builder/ref-proxy.ts +22 -4
  247. package/src/query/builder/types.ts +257 -62
  248. package/src/query/compiler/evaluators.ts +57 -0
  249. package/src/query/compiler/group-by.ts +156 -35
  250. package/src/query/compiler/index.ts +445 -15
  251. package/src/query/compiler/order-by.ts +51 -12
  252. package/src/query/compiler/select.ts +9 -0
  253. package/src/query/index.ts +7 -0
  254. package/src/query/ir.ts +23 -2
  255. package/src/query/live/collection-config-builder.ts +809 -9
  256. package/src/query/live/types.ts +10 -4
  257. package/src/query/live/utils.ts +64 -3
  258. package/src/query/live-query-collection.ts +43 -18
  259. package/src/query/query-once.ts +31 -12
  260. package/src/query/subset-dedupe.ts +11 -7
  261. package/src/types.ts +49 -9
  262. package/src/utils/array-utils.ts +49 -0
  263. package/src/utils/comparison.ts +14 -0
  264. package/src/utils/index-optimization.ts +4 -0
  265. package/src/utils.ts +12 -9
  266. package/src/virtual-props.ts +282 -0
  267. package/dist/cjs/indexes/lazy-index.cjs +0 -190
  268. package/dist/cjs/indexes/lazy-index.cjs.map +0 -1
  269. package/dist/cjs/indexes/lazy-index.d.cts +0 -96
  270. package/dist/esm/indexes/lazy-index.d.ts +0 -96
  271. package/dist/esm/indexes/lazy-index.js +0 -190
  272. package/dist/esm/indexes/lazy-index.js.map +0 -1
  273. package/src/indexes/lazy-index.ts +0 -251
@@ -1,16 +1,210 @@
1
- import { IndexProxy, LazyIndexWrapper } from '../indexes/lazy-index'
2
1
  import {
3
2
  createSingleRowRefProxy,
4
3
  toExpression,
5
4
  } from '../query/builder/ref-proxy'
6
- import { BTreeIndex } from '../indexes/btree-index'
5
+ import { CollectionConfigurationError } from '../errors'
7
6
  import type { StandardSchemaV1 } from '@standard-schema/spec'
8
- import type { BaseIndex, IndexResolver } from '../indexes/base-index'
7
+ import type { BaseIndex, IndexConstructor } from '../indexes/base-index'
9
8
  import type { ChangeMessage } from '../types'
10
9
  import type { IndexOptions } from '../indexes/index-options'
11
10
  import type { SingleRowRefProxy } from '../query/builder/ref-proxy'
12
11
  import type { CollectionLifecycleManager } from './lifecycle'
13
12
  import type { CollectionStateManager } from './state'
13
+ import type { BasicExpression } from '../query/ir'
14
+ import type {
15
+ CollectionEventsManager,
16
+ CollectionIndexMetadata,
17
+ CollectionIndexResolverMetadata,
18
+ CollectionIndexSerializableValue,
19
+ } from './events'
20
+
21
+ const INDEX_SIGNATURE_VERSION = 1 as const
22
+
23
+ function compareStringsCodePoint(left: string, right: string): number {
24
+ if (left === right) {
25
+ return 0
26
+ }
27
+
28
+ return left < right ? -1 : 1
29
+ }
30
+
31
+ function resolveResolverMetadata<TKey extends string | number>(
32
+ resolver: IndexConstructor<TKey>,
33
+ ): CollectionIndexResolverMetadata {
34
+ return {
35
+ kind: `constructor`,
36
+ ...(resolver.name ? { name: resolver.name } : {}),
37
+ }
38
+ }
39
+
40
+ function toSerializableIndexValue(
41
+ value: unknown,
42
+ ): CollectionIndexSerializableValue | undefined {
43
+ if (value == null) {
44
+ return value
45
+ }
46
+
47
+ switch (typeof value) {
48
+ case `string`:
49
+ case `boolean`:
50
+ return value
51
+ case `number`:
52
+ return Number.isFinite(value) ? value : null
53
+ case `bigint`:
54
+ return { __type: `bigint`, value: value.toString() }
55
+ case `function`:
56
+ case `symbol`:
57
+ // Function and symbol identity are process-local and not stable across runtimes.
58
+ // Dropping them keeps signatures deterministic; we may skip index reuse, which is acceptable.
59
+ return undefined
60
+ case `undefined`:
61
+ return undefined
62
+ }
63
+
64
+ if (Array.isArray(value)) {
65
+ return value.map((entry) => toSerializableIndexValue(entry) ?? null)
66
+ }
67
+
68
+ if (value instanceof Date) {
69
+ return {
70
+ __type: `date`,
71
+ value: value.toISOString(),
72
+ }
73
+ }
74
+
75
+ if (value instanceof Set) {
76
+ const serializedValues = Array.from(value)
77
+ .map((entry) => toSerializableIndexValue(entry) ?? null)
78
+ .sort((a, b) =>
79
+ compareStringsCodePoint(
80
+ stableStringifyCollectionIndexValue(a),
81
+ stableStringifyCollectionIndexValue(b),
82
+ ),
83
+ )
84
+ return {
85
+ __type: `set`,
86
+ values: serializedValues,
87
+ }
88
+ }
89
+
90
+ if (value instanceof Map) {
91
+ const serializedEntries = Array.from(value.entries())
92
+ .map(([mapKey, mapValue]) => ({
93
+ key: toSerializableIndexValue(mapKey) ?? null,
94
+ value: toSerializableIndexValue(mapValue) ?? null,
95
+ }))
96
+ .sort((a, b) =>
97
+ compareStringsCodePoint(
98
+ stableStringifyCollectionIndexValue(a.key),
99
+ stableStringifyCollectionIndexValue(b.key),
100
+ ),
101
+ )
102
+
103
+ return {
104
+ __type: `map`,
105
+ entries: serializedEntries,
106
+ }
107
+ }
108
+
109
+ if (value instanceof RegExp) {
110
+ return {
111
+ __type: `regexp`,
112
+ value: value.toString(),
113
+ }
114
+ }
115
+
116
+ const serializedObject: Record<string, CollectionIndexSerializableValue> = {}
117
+ const entries = Object.entries(value as Record<string, unknown>).sort(
118
+ ([leftKey], [rightKey]) => compareStringsCodePoint(leftKey, rightKey),
119
+ )
120
+
121
+ for (const [key, entryValue] of entries) {
122
+ const serializedEntry = toSerializableIndexValue(entryValue)
123
+ if (serializedEntry !== undefined) {
124
+ serializedObject[key] = serializedEntry
125
+ }
126
+ }
127
+
128
+ return serializedObject
129
+ }
130
+
131
+ function stableStringifyCollectionIndexValue(
132
+ value: CollectionIndexSerializableValue,
133
+ ): string {
134
+ if (value === null) {
135
+ return `null`
136
+ }
137
+
138
+ if (Array.isArray(value)) {
139
+ return `[${value.map(stableStringifyCollectionIndexValue).join(`,`)}]`
140
+ }
141
+
142
+ if (typeof value !== `object`) {
143
+ return JSON.stringify(value)
144
+ }
145
+
146
+ const sortedKeys = Object.keys(value).sort((left, right) =>
147
+ compareStringsCodePoint(left, right),
148
+ )
149
+ const serializedEntries = sortedKeys.map(
150
+ (key) =>
151
+ `${JSON.stringify(key)}:${stableStringifyCollectionIndexValue(value[key]!)}`,
152
+ )
153
+ return `{${serializedEntries.join(`,`)}}`
154
+ }
155
+
156
+ function createCollectionIndexMetadata<TKey extends string | number>(
157
+ indexId: number,
158
+ expression: BasicExpression,
159
+ name: string | undefined,
160
+ resolver: IndexConstructor<TKey>,
161
+ options: unknown,
162
+ ): CollectionIndexMetadata {
163
+ const resolverMetadata = resolveResolverMetadata(resolver)
164
+ const serializedExpression = toSerializableIndexValue(expression) ?? null
165
+ const serializedOptions = toSerializableIndexValue(options)
166
+ const signatureInput = toSerializableIndexValue({
167
+ signatureVersion: INDEX_SIGNATURE_VERSION,
168
+ expression: serializedExpression,
169
+ options: serializedOptions ?? null,
170
+ })
171
+ const normalizedSignatureInput = signatureInput ?? null
172
+ const signature = stableStringifyCollectionIndexValue(
173
+ normalizedSignatureInput,
174
+ )
175
+
176
+ return {
177
+ signatureVersion: INDEX_SIGNATURE_VERSION,
178
+ signature,
179
+ indexId,
180
+ name,
181
+ expression,
182
+ resolver: resolverMetadata,
183
+ ...(serializedOptions === undefined ? {} : { options: serializedOptions }),
184
+ }
185
+ }
186
+
187
+ function cloneSerializableIndexValue(
188
+ value: CollectionIndexSerializableValue,
189
+ ): CollectionIndexSerializableValue {
190
+ if (value === null || typeof value !== `object`) {
191
+ return value
192
+ }
193
+
194
+ if (Array.isArray(value)) {
195
+ return value.map((entry) => cloneSerializableIndexValue(entry))
196
+ }
197
+
198
+ const cloned: Record<string, CollectionIndexSerializableValue> = {}
199
+ for (const [key, entryValue] of Object.entries(value)) {
200
+ cloned[key] = cloneSerializableIndexValue(entryValue)
201
+ }
202
+ return cloned
203
+ }
204
+
205
+ function cloneExpression(expression: BasicExpression): BasicExpression {
206
+ return JSON.parse(JSON.stringify(expression)) as BasicExpression
207
+ }
14
208
 
15
209
  export class CollectionIndexesManager<
16
210
  TOutput extends object = Record<string, unknown>,
@@ -20,10 +214,11 @@ export class CollectionIndexesManager<
20
214
  > {
21
215
  private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
22
216
  private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>
217
+ private defaultIndexType: IndexConstructor<TKey> | undefined
218
+ private events!: CollectionEventsManager
23
219
 
24
- public lazyIndexes = new Map<number, LazyIndexWrapper<TKey>>()
25
- public resolvedIndexes = new Map<number, BaseIndex<TKey>>()
26
- public isIndexesResolved = false
220
+ public indexes = new Map<number, BaseIndex<TKey>>()
221
+ public indexMetadata = new Map<number, CollectionIndexMetadata>()
27
222
  public indexCounter = 0
28
223
 
29
224
  constructor() {}
@@ -31,18 +226,32 @@ export class CollectionIndexesManager<
31
226
  setDeps(deps: {
32
227
  state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
33
228
  lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
229
+ defaultIndexType?: IndexConstructor<TKey>
230
+ events: CollectionEventsManager
34
231
  }) {
35
232
  this.state = deps.state
36
233
  this.lifecycle = deps.lifecycle
234
+ this.defaultIndexType = deps.defaultIndexType
235
+ this.events = deps.events
37
236
  }
38
237
 
39
238
  /**
40
239
  * Creates an index on a collection for faster queries.
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * // With explicit index type (recommended for tree-shaking)
244
+ * import { BasicIndex } from '@tanstack/db'
245
+ * collection.createIndex((row) => row.userId, { indexType: BasicIndex })
246
+ *
247
+ * // With collection's default index type
248
+ * collection.createIndex((row) => row.userId)
249
+ * ```
41
250
  */
42
- public createIndex<TResolver extends IndexResolver<TKey> = typeof BTreeIndex>(
251
+ public createIndex<TIndexType extends IndexConstructor<TKey>>(
43
252
  indexCallback: (row: SingleRowRefProxy<TOutput>) => any,
44
- config: IndexOptions<TResolver> = {},
45
- ): IndexProxy<TKey> {
253
+ config: IndexOptions<TIndexType> = {},
254
+ ): BaseIndex<TKey> {
46
255
  this.lifecycle.validateCollectionUsable(`createIndex`)
47
256
 
48
257
  const indexId = ++this.indexCounter
@@ -50,97 +259,96 @@ export class CollectionIndexesManager<
50
259
  const indexExpression = indexCallback(singleRowRefProxy)
51
260
  const expression = toExpression(indexExpression)
52
261
 
53
- // Default to BTreeIndex if no type specified
54
- const resolver = config.indexType ?? (BTreeIndex as unknown as TResolver)
262
+ // Use provided index type, or fall back to collection's default
263
+ const IndexType = config.indexType ?? this.defaultIndexType
264
+ if (!IndexType) {
265
+ throw new CollectionConfigurationError(
266
+ `No index type specified and no defaultIndexType set on collection. ` +
267
+ `Either pass indexType in config, or set defaultIndexType on the collection:\n` +
268
+ ` import { BasicIndex } from '@tanstack/db'\n` +
269
+ ` createCollection({ defaultIndexType: BasicIndex, ... })`,
270
+ )
271
+ }
55
272
 
56
- // Create lazy wrapper
57
- const lazyIndex = new LazyIndexWrapper<TKey>(
273
+ // Create index synchronously
274
+ const index = new IndexType(
58
275
  indexId,
59
276
  expression,
60
277
  config.name,
61
- resolver,
62
278
  config.options,
63
- this.state.entries(),
64
279
  )
65
280
 
66
- this.lazyIndexes.set(indexId, lazyIndex)
281
+ // Build with current data
282
+ index.build(this.state.entries())
67
283
 
68
- // For BTreeIndex, resolve immediately and synchronously
69
- if ((resolver as unknown) === BTreeIndex) {
70
- try {
71
- const resolvedIndex = lazyIndex.getResolved()
72
- this.resolvedIndexes.set(indexId, resolvedIndex)
73
- } catch (error) {
74
- console.warn(`Failed to resolve BTreeIndex:`, error)
75
- }
76
- } else if (typeof resolver === `function` && resolver.prototype) {
77
- // Other synchronous constructors - resolve immediately
78
- try {
79
- const resolvedIndex = lazyIndex.getResolved()
80
- this.resolvedIndexes.set(indexId, resolvedIndex)
81
- } catch {
82
- // Fallback to async resolution
83
- this.resolveSingleIndex(indexId, lazyIndex).catch((error) => {
84
- console.warn(`Failed to resolve single index:`, error)
85
- })
86
- }
87
- } else if (this.isIndexesResolved) {
88
- // Async loader but indexes are already resolved - resolve this one
89
- this.resolveSingleIndex(indexId, lazyIndex).catch((error) => {
90
- console.warn(`Failed to resolve single index:`, error)
91
- })
92
- }
284
+ this.indexes.set(indexId, index)
93
285
 
94
- return new IndexProxy(indexId, lazyIndex)
286
+ // Track metadata and emit event
287
+ const metadata = createCollectionIndexMetadata(
288
+ indexId,
289
+ expression,
290
+ config.name,
291
+ IndexType,
292
+ config.options,
293
+ )
294
+ this.indexMetadata.set(indexId, metadata)
295
+ this.events.emitIndexAdded(metadata)
296
+
297
+ return index
95
298
  }
96
299
 
97
300
  /**
98
- * Resolve all lazy indexes (called when collection first syncs)
301
+ * Removes an index from this collection.
302
+ * Returns true when an index existed and was removed, false otherwise.
99
303
  */
100
- public async resolveAllIndexes(): Promise<void> {
101
- if (this.isIndexesResolved) return
304
+ public removeIndex(indexOrId: BaseIndex<TKey> | number): boolean {
305
+ this.lifecycle.validateCollectionUsable(`removeIndex`)
102
306
 
103
- const resolutionPromises = Array.from(this.lazyIndexes.entries()).map(
104
- async ([indexId, lazyIndex]) => {
105
- const resolvedIndex = await lazyIndex.resolve()
307
+ const indexId = typeof indexOrId === `number` ? indexOrId : indexOrId.id
308
+ const index = this.indexes.get(indexId)
309
+ if (!index) {
310
+ return false
311
+ }
106
312
 
107
- // Build index with current data
108
- resolvedIndex.build(this.state.entries())
313
+ if (typeof indexOrId !== `number` && index !== indexOrId) {
314
+ // Passed a different index instance with the same id — do not remove.
315
+ return false
316
+ }
109
317
 
110
- this.resolvedIndexes.set(indexId, resolvedIndex)
111
- return { indexId, resolvedIndex }
112
- },
113
- )
318
+ this.indexes.delete(indexId)
114
319
 
115
- await Promise.all(resolutionPromises)
116
- this.isIndexesResolved = true
117
- }
320
+ const metadata = this.indexMetadata.get(indexId)
321
+ this.indexMetadata.delete(indexId)
322
+ if (metadata) {
323
+ this.events.emitIndexRemoved(metadata)
324
+ }
118
325
 
119
- /**
120
- * Resolve a single index immediately
121
- */
122
- private async resolveSingleIndex(
123
- indexId: number,
124
- lazyIndex: LazyIndexWrapper<TKey>,
125
- ): Promise<BaseIndex<TKey>> {
126
- const resolvedIndex = await lazyIndex.resolve()
127
- resolvedIndex.build(this.state.entries())
128
- this.resolvedIndexes.set(indexId, resolvedIndex)
129
- return resolvedIndex
326
+ return true
130
327
  }
131
328
 
132
329
  /**
133
- * Get resolved indexes for query optimization
330
+ * Returns a sorted snapshot of index metadata.
331
+ * This allows persisted wrappers to bootstrap from indexes that were created
332
+ * before they attached lifecycle listeners.
134
333
  */
135
- get indexes(): Map<number, BaseIndex<TKey>> {
136
- return this.resolvedIndexes
334
+ public getIndexMetadataSnapshot(): Array<CollectionIndexMetadata> {
335
+ return Array.from(this.indexMetadata.values())
336
+ .sort((left, right) => left.indexId - right.indexId)
337
+ .map((metadata) => ({
338
+ ...metadata,
339
+ expression: cloneExpression(metadata.expression),
340
+ resolver: { ...metadata.resolver },
341
+ ...(metadata.options === undefined
342
+ ? {}
343
+ : { options: cloneSerializableIndexValue(metadata.options) }),
344
+ }))
137
345
  }
138
346
 
139
347
  /**
140
348
  * Updates all indexes when the collection changes
141
349
  */
142
350
  public updateIndexes(changes: Array<ChangeMessage<TOutput, TKey>>): void {
143
- for (const index of this.resolvedIndexes.values()) {
351
+ for (const index of this.indexes.values()) {
144
352
  for (const change of changes) {
145
353
  switch (change.type) {
146
354
  case `insert`:
@@ -162,11 +370,10 @@ export class CollectionIndexesManager<
162
370
  }
163
371
 
164
372
  /**
165
- * Clean up the collection by stopping sync and clearing data
166
- * This can be called manually or automatically by garbage collection
373
+ * Clean up indexes
167
374
  */
168
375
  public cleanup(): void {
169
- this.lazyIndexes.clear()
170
- this.resolvedIndexes.clear()
376
+ this.indexes.clear()
377
+ this.indexMetadata.clear()
171
378
  }
172
379
  }
@@ -7,6 +7,7 @@ import {
7
7
  safeCancelIdleCallback,
8
8
  safeRequestIdleCallback,
9
9
  } from '../utils/browser-polyfills'
10
+ import { CleanupQueue } from './cleanup-queue'
10
11
  import type { IdleCallbackDeadline } from '../utils/browser-polyfills'
11
12
  import type { StandardSchemaV1 } from '@standard-schema/spec'
12
13
  import type { CollectionConfig, CollectionStatus } from '../types'
@@ -34,7 +35,6 @@ export class CollectionLifecycleManager<
34
35
  public hasBeenReady = false
35
36
  public hasReceivedFirstCommit = false
36
37
  public onFirstReadyCallbacks: Array<() => void> = []
37
- public gcTimeoutId: ReturnType<typeof setTimeout> | null = null
38
38
  private idleCallbackId: number | null = null
39
39
 
40
40
  /**
@@ -106,17 +106,6 @@ export class CollectionLifecycleManager<
106
106
  const previousStatus = this.status
107
107
  this.status = newStatus
108
108
 
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
-
120
109
  // Emit event
121
110
  this.events.emitStatusChange(newStatus, previousStatus)
122
111
  }
@@ -174,10 +163,6 @@ export class CollectionLifecycleManager<
174
163
  * Called when the collection becomes inactive (no subscribers)
175
164
  */
176
165
  public startGCTimer(): void {
177
- if (this.gcTimeoutId) {
178
- clearTimeout(this.gcTimeoutId)
179
- }
180
-
181
166
  const gcTime = this.config.gcTime ?? 300000 // 5 minutes default
182
167
 
183
168
  // If gcTime is 0, negative, or non-finite (Infinity, -Infinity, NaN), GC is disabled.
@@ -187,12 +172,12 @@ export class CollectionLifecycleManager<
187
172
  return
188
173
  }
189
174
 
190
- this.gcTimeoutId = setTimeout(() => {
175
+ CleanupQueue.getInstance().schedule(this, gcTime, () => {
191
176
  if (this.changes.activeSubscribersCount === 0) {
192
177
  // Schedule cleanup during idle time to avoid blocking the UI thread
193
178
  this.scheduleIdleCleanup()
194
179
  }
195
- }, gcTime)
180
+ })
196
181
  }
197
182
 
198
183
  /**
@@ -200,10 +185,7 @@ export class CollectionLifecycleManager<
200
185
  * Called when the collection becomes active again
201
186
  */
202
187
  public cancelGCTimer(): void {
203
- if (this.gcTimeoutId) {
204
- clearTimeout(this.gcTimeoutId)
205
- this.gcTimeoutId = null
206
- }
188
+ CleanupQueue.getInstance().cancel(this)
207
189
  // Also cancel any pending idle cleanup
208
190
  if (this.idleCallbackId !== null) {
209
191
  safeCancelIdleCallback(this.idleCallbackId)
@@ -258,10 +240,7 @@ export class CollectionLifecycleManager<
258
240
  this.changes.cleanup()
259
241
  this.indexes.cleanup()
260
242
 
261
- if (this.gcTimeoutId) {
262
- clearTimeout(this.gcTimeoutId)
263
- this.gcTimeoutId = null
264
- }
243
+ CleanupQueue.getInstance().cancel(this)
265
244
 
266
245
  this.hasBeenReady = false
267
246
 
@@ -17,6 +17,7 @@ import {
17
17
  UndefinedKeyError,
18
18
  UpdateKeyNotFoundError,
19
19
  } from '../errors'
20
+ import { DIRECT_TRANSACTION_METADATA_KEY } from './transaction-metadata.js'
20
21
  import type { Collection, CollectionImpl } from './index.js'
21
22
  import type { StandardSchemaV1 } from '@standard-schema/spec'
22
23
  import type {
@@ -153,6 +154,14 @@ export class CollectionMutationsManager<
153
154
  return `KEY::${this.id}/${key}`
154
155
  }
155
156
 
157
+ private markPendingLocalOrigins(
158
+ mutations: Array<PendingMutation<TOutput>>,
159
+ ): void {
160
+ for (const mutation of mutations) {
161
+ this.state.pendingLocalOrigins.add(mutation.key as TKey)
162
+ }
163
+ }
164
+
156
165
  /**
157
166
  * Inserts one or more items into the collection
158
167
  */
@@ -222,6 +231,9 @@ export class CollectionMutationsManager<
222
231
  } else {
223
232
  // Create a new transaction with a mutation function that calls the onInsert handler
224
233
  const directOpTransaction = createTransaction<TOutput>({
234
+ metadata: {
235
+ [DIRECT_TRANSACTION_METADATA_KEY]: true,
236
+ },
225
237
  mutationFn: async (params) => {
226
238
  // Call the onInsert handler with the transaction and collection
227
239
  return await this.config.onInsert!({
@@ -237,6 +249,7 @@ export class CollectionMutationsManager<
237
249
 
238
250
  // Apply mutations to the new transaction
239
251
  directOpTransaction.applyMutations(mutations)
252
+ this.markPendingLocalOrigins(mutations)
240
253
  // Errors still reject tx.isPersisted.promise; this catch only prevents global unhandled rejections
241
254
  directOpTransaction.commit().catch(() => undefined)
242
255
 
@@ -417,6 +430,9 @@ export class CollectionMutationsManager<
417
430
 
418
431
  // Create a new transaction with a mutation function that calls the onUpdate handler
419
432
  const directOpTransaction = createTransaction<TOutput>({
433
+ metadata: {
434
+ [DIRECT_TRANSACTION_METADATA_KEY]: true,
435
+ },
420
436
  mutationFn: async (params) => {
421
437
  // Call the onUpdate handler with the transaction and collection
422
438
  return this.config.onUpdate!({
@@ -432,6 +448,7 @@ export class CollectionMutationsManager<
432
448
 
433
449
  // Apply mutations to the new transaction
434
450
  directOpTransaction.applyMutations(mutations)
451
+ this.markPendingLocalOrigins(mutations)
435
452
  // Errors still hit tx.isPersisted.promise; avoid leaking an unhandled rejection from the fire-and-forget commit
436
453
  directOpTransaction.commit().catch(() => undefined)
437
454
 
@@ -519,6 +536,9 @@ export class CollectionMutationsManager<
519
536
  // Create a new transaction with a mutation function that calls the onDelete handler
520
537
  const directOpTransaction = createTransaction<TOutput>({
521
538
  autoCommit: true,
539
+ metadata: {
540
+ [DIRECT_TRANSACTION_METADATA_KEY]: true,
541
+ },
522
542
  mutationFn: async (params) => {
523
543
  // Call the onDelete handler with the transaction and collection
524
544
  return this.config.onDelete!({
@@ -534,6 +554,7 @@ export class CollectionMutationsManager<
534
554
 
535
555
  // Apply mutations to the new transaction
536
556
  directOpTransaction.applyMutations(mutations)
557
+ this.markPendingLocalOrigins(mutations)
537
558
  // Errors still reject tx.isPersisted.promise; silence the internal commit promise to prevent test noise
538
559
  directOpTransaction.commit().catch(() => undefined)
539
560