@tanstack/db 0.3.2 → 0.4.1

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 (171) hide show
  1. package/dist/cjs/{change-events.cjs → collection/change-events.cjs} +13 -42
  2. package/dist/cjs/collection/change-events.cjs.map +1 -0
  3. package/dist/{esm/change-events.d.ts → cjs/collection/change-events.d.cts} +6 -6
  4. package/dist/cjs/collection/changes.cjs +108 -0
  5. package/dist/cjs/collection/changes.cjs.map +1 -0
  6. package/dist/cjs/collection/changes.d.cts +53 -0
  7. package/dist/cjs/{collection-events.cjs → collection/events.cjs} +7 -5
  8. package/dist/cjs/collection/events.cjs.map +1 -0
  9. package/dist/cjs/{collection-events.d.cts → collection/events.d.cts} +7 -4
  10. package/dist/cjs/collection/index.cjs +417 -0
  11. package/dist/cjs/collection/index.cjs.map +1 -0
  12. package/dist/{esm/collection.d.ts → cjs/collection/index.d.cts} +46 -184
  13. package/dist/cjs/collection/indexes.cjs +124 -0
  14. package/dist/cjs/collection/indexes.cjs.map +1 -0
  15. package/dist/cjs/collection/indexes.d.cts +47 -0
  16. package/dist/cjs/collection/lifecycle.cjs +196 -0
  17. package/dist/cjs/collection/lifecycle.cjs.map +1 -0
  18. package/dist/cjs/collection/lifecycle.d.cts +81 -0
  19. package/dist/cjs/collection/mutations.cjs +315 -0
  20. package/dist/cjs/collection/mutations.cjs.map +1 -0
  21. package/dist/cjs/collection/mutations.d.cts +33 -0
  22. package/dist/cjs/collection/state.cjs +597 -0
  23. package/dist/cjs/collection/state.cjs.map +1 -0
  24. package/dist/cjs/collection/state.d.cts +122 -0
  25. package/dist/cjs/collection/subscription.cjs +160 -0
  26. package/dist/cjs/collection/subscription.cjs.map +1 -0
  27. package/dist/cjs/collection/subscription.d.cts +57 -0
  28. package/dist/cjs/collection/sync.cjs +154 -0
  29. package/dist/cjs/collection/sync.cjs.map +1 -0
  30. package/dist/cjs/collection/sync.d.cts +34 -0
  31. package/dist/cjs/index.cjs +8 -8
  32. package/dist/cjs/index.d.cts +2 -2
  33. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  34. package/dist/cjs/indexes/auto-index.d.cts +1 -1
  35. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  36. package/dist/cjs/indexes/base-index.d.cts +2 -2
  37. package/dist/cjs/indexes/btree-index.cjs +2 -2
  38. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  39. package/dist/cjs/indexes/btree-index.d.cts +1 -1
  40. package/dist/cjs/query/builder/index.cjs +2 -2
  41. package/dist/cjs/query/builder/index.cjs.map +1 -1
  42. package/dist/cjs/query/builder/types.d.cts +1 -1
  43. package/dist/cjs/query/compiler/index.cjs +5 -2
  44. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  45. package/dist/cjs/query/compiler/index.d.cts +3 -2
  46. package/dist/cjs/query/compiler/joins.cjs +22 -24
  47. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  48. package/dist/cjs/query/compiler/joins.d.cts +3 -2
  49. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  50. package/dist/cjs/query/compiler/order-by.d.cts +1 -1
  51. package/dist/cjs/query/ir.cjs.map +1 -1
  52. package/dist/cjs/query/ir.d.cts +1 -1
  53. package/dist/cjs/query/live/collection-config-builder.cjs +29 -12
  54. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  55. package/dist/cjs/query/live/collection-config-builder.d.cts +3 -0
  56. package/dist/cjs/query/live/collection-subscriber.cjs +43 -104
  57. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  58. package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
  59. package/dist/cjs/query/live-query-collection.cjs +2 -2
  60. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  61. package/dist/cjs/query/live-query-collection.d.cts +1 -1
  62. package/dist/cjs/transactions.cjs +3 -3
  63. package/dist/cjs/transactions.cjs.map +1 -1
  64. package/dist/cjs/types.d.cts +12 -10
  65. package/dist/cjs/utils/browser-polyfills.cjs +22 -0
  66. package/dist/cjs/utils/browser-polyfills.cjs.map +1 -0
  67. package/dist/cjs/utils/browser-polyfills.d.cts +9 -0
  68. package/dist/{cjs/change-events.d.cts → esm/collection/change-events.d.ts} +6 -6
  69. package/dist/esm/{change-events.js → collection/change-events.js} +13 -42
  70. package/dist/esm/collection/change-events.js.map +1 -0
  71. package/dist/esm/collection/changes.d.ts +53 -0
  72. package/dist/esm/collection/changes.js +108 -0
  73. package/dist/esm/collection/changes.js.map +1 -0
  74. package/dist/esm/{collection-events.d.ts → collection/events.d.ts} +7 -4
  75. package/dist/esm/{collection-events.js → collection/events.js} +7 -5
  76. package/dist/esm/collection/events.js.map +1 -0
  77. package/dist/{cjs/collection.d.cts → esm/collection/index.d.ts} +46 -184
  78. package/dist/esm/collection/index.js +417 -0
  79. package/dist/esm/collection/index.js.map +1 -0
  80. package/dist/esm/collection/indexes.d.ts +47 -0
  81. package/dist/esm/collection/indexes.js +124 -0
  82. package/dist/esm/collection/indexes.js.map +1 -0
  83. package/dist/esm/collection/lifecycle.d.ts +81 -0
  84. package/dist/esm/collection/lifecycle.js +196 -0
  85. package/dist/esm/collection/lifecycle.js.map +1 -0
  86. package/dist/esm/collection/mutations.d.ts +33 -0
  87. package/dist/esm/collection/mutations.js +315 -0
  88. package/dist/esm/collection/mutations.js.map +1 -0
  89. package/dist/esm/collection/state.d.ts +122 -0
  90. package/dist/esm/collection/state.js +597 -0
  91. package/dist/esm/collection/state.js.map +1 -0
  92. package/dist/esm/collection/subscription.d.ts +57 -0
  93. package/dist/esm/collection/subscription.js +160 -0
  94. package/dist/esm/collection/subscription.js.map +1 -0
  95. package/dist/esm/collection/sync.d.ts +34 -0
  96. package/dist/esm/collection/sync.js +154 -0
  97. package/dist/esm/collection/sync.js.map +1 -0
  98. package/dist/esm/index.d.ts +2 -2
  99. package/dist/esm/index.js +1 -1
  100. package/dist/esm/indexes/auto-index.d.ts +1 -1
  101. package/dist/esm/indexes/auto-index.js.map +1 -1
  102. package/dist/esm/indexes/base-index.d.ts +2 -2
  103. package/dist/esm/indexes/base-index.js.map +1 -1
  104. package/dist/esm/indexes/btree-index.d.ts +1 -1
  105. package/dist/esm/indexes/btree-index.js +2 -2
  106. package/dist/esm/indexes/btree-index.js.map +1 -1
  107. package/dist/esm/proxy.js +1 -1
  108. package/dist/esm/query/builder/index.js +1 -1
  109. package/dist/esm/query/builder/index.js.map +1 -1
  110. package/dist/esm/query/builder/types.d.ts +1 -1
  111. package/dist/esm/query/compiler/index.d.ts +3 -2
  112. package/dist/esm/query/compiler/index.js +5 -2
  113. package/dist/esm/query/compiler/index.js.map +1 -1
  114. package/dist/esm/query/compiler/joins.d.ts +3 -2
  115. package/dist/esm/query/compiler/joins.js +22 -24
  116. package/dist/esm/query/compiler/joins.js.map +1 -1
  117. package/dist/esm/query/compiler/order-by.d.ts +1 -1
  118. package/dist/esm/query/compiler/order-by.js.map +1 -1
  119. package/dist/esm/query/ir.d.ts +1 -1
  120. package/dist/esm/query/ir.js.map +1 -1
  121. package/dist/esm/query/live/collection-config-builder.d.ts +3 -0
  122. package/dist/esm/query/live/collection-config-builder.js +29 -12
  123. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  124. package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
  125. package/dist/esm/query/live/collection-subscriber.js +43 -104
  126. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  127. package/dist/esm/query/live-query-collection.d.ts +1 -1
  128. package/dist/esm/query/live-query-collection.js +1 -1
  129. package/dist/esm/query/live-query-collection.js.map +1 -1
  130. package/dist/esm/transactions.js +3 -3
  131. package/dist/esm/transactions.js.map +1 -1
  132. package/dist/esm/types.d.ts +12 -10
  133. package/dist/esm/utils/browser-polyfills.d.ts +9 -0
  134. package/dist/esm/utils/browser-polyfills.js +22 -0
  135. package/dist/esm/utils/browser-polyfills.js.map +1 -0
  136. package/package.json +2 -2
  137. package/src/{change-events.ts → collection/change-events.ts} +25 -39
  138. package/src/collection/changes.ts +163 -0
  139. package/src/{collection-events.ts → collection/events.ts} +8 -6
  140. package/src/collection/index.ts +808 -0
  141. package/src/collection/indexes.ts +172 -0
  142. package/src/collection/lifecycle.ts +289 -0
  143. package/src/collection/mutations.ts +535 -0
  144. package/src/collection/state.ts +866 -0
  145. package/src/collection/subscription.ts +239 -0
  146. package/src/collection/sync.ts +235 -0
  147. package/src/index.ts +2 -2
  148. package/src/indexes/auto-index.ts +1 -1
  149. package/src/indexes/base-index.ts +3 -3
  150. package/src/indexes/btree-index.ts +2 -2
  151. package/src/query/builder/index.ts +1 -1
  152. package/src/query/builder/types.ts +1 -1
  153. package/src/query/compiler/index.ts +7 -1
  154. package/src/query/compiler/joins.ts +28 -41
  155. package/src/query/compiler/order-by.ts +1 -1
  156. package/src/query/ir.ts +1 -1
  157. package/src/query/live/collection-config-builder.ts +48 -22
  158. package/src/query/live/collection-subscriber.ts +63 -168
  159. package/src/query/live-query-collection.ts +2 -2
  160. package/src/transactions.ts +3 -3
  161. package/src/types.ts +14 -15
  162. package/src/utils/browser-polyfills.ts +39 -0
  163. package/dist/cjs/change-events.cjs.map +0 -1
  164. package/dist/cjs/collection-events.cjs.map +0 -1
  165. package/dist/cjs/collection.cjs +0 -1625
  166. package/dist/cjs/collection.cjs.map +0 -1
  167. package/dist/esm/change-events.js.map +0 -1
  168. package/dist/esm/collection-events.js.map +0 -1
  169. package/dist/esm/collection.js +0 -1625
  170. package/dist/esm/collection.js.map +0 -1
  171. package/src/collection.ts +0 -2564
@@ -0,0 +1,239 @@
1
+ import { ensureIndexForExpression } from "../indexes/auto-index.js"
2
+ import { and } from "../query/index.js"
3
+ import {
4
+ createFilterFunctionFromExpression,
5
+ createFilteredCallback,
6
+ } from "./change-events.js"
7
+ import type { BasicExpression } from "../query/ir.js"
8
+ import type { BaseIndex } from "../indexes/base-index.js"
9
+ import type { ChangeMessage } from "../types.js"
10
+ import type { CollectionImpl } from "./index.js"
11
+
12
+ type RequestSnapshotOptions = {
13
+ where?: BasicExpression<boolean>
14
+ optimizedOnly?: boolean
15
+ }
16
+
17
+ type RequestLimitedSnapshotOptions = {
18
+ minValue?: any
19
+ limit: number
20
+ }
21
+
22
+ type CollectionSubscriptionOptions = {
23
+ /** Pre-compiled expression for filtering changes */
24
+ whereExpression?: BasicExpression<boolean>
25
+ /** Callback to call when the subscription is unsubscribed */
26
+ onUnsubscribe?: () => void
27
+ }
28
+
29
+ export class CollectionSubscription {
30
+ private loadedInitialState = false
31
+
32
+ // Flag to indicate that we have sent at least 1 snapshot.
33
+ // While `snapshotSent` is false we filter out all changes from subscription to the collection.
34
+ private snapshotSent = false
35
+
36
+ // Keep track of the keys we've sent (needed for join and orderBy optimizations)
37
+ private sentKeys = new Set<string | number>()
38
+
39
+ private filteredCallback: (changes: Array<ChangeMessage<any, any>>) => void
40
+
41
+ private orderByIndex: BaseIndex<string | number> | undefined
42
+
43
+ constructor(
44
+ private collection: CollectionImpl<any, any, any, any, any>,
45
+ private callback: (changes: Array<ChangeMessage<any, any>>) => void,
46
+ private options: CollectionSubscriptionOptions
47
+ ) {
48
+ // Auto-index for where expressions if enabled
49
+ if (options.whereExpression) {
50
+ ensureIndexForExpression(options.whereExpression, this.collection)
51
+ }
52
+
53
+ const callbackWithSentKeysTracking = (
54
+ changes: Array<ChangeMessage<any, any>>
55
+ ) => {
56
+ callback(changes)
57
+ this.trackSentKeys(changes)
58
+ }
59
+
60
+ this.callback = callbackWithSentKeysTracking
61
+
62
+ // Create a filtered callback if where clause is provided
63
+ this.filteredCallback = options.whereExpression
64
+ ? createFilteredCallback(this.callback, options)
65
+ : this.callback
66
+ }
67
+
68
+ setOrderByIndex(index: BaseIndex<any>) {
69
+ this.orderByIndex = index
70
+ }
71
+
72
+ hasLoadedInitialState() {
73
+ return this.loadedInitialState
74
+ }
75
+
76
+ hasSentAtLeastOneSnapshot() {
77
+ return this.snapshotSent
78
+ }
79
+
80
+ emitEvents(changes: Array<ChangeMessage<any, any>>) {
81
+ const newChanges = this.filterAndFlipChanges(changes)
82
+ this.filteredCallback(newChanges)
83
+ }
84
+
85
+ /**
86
+ * Sends the snapshot to the callback.
87
+ * Returns a boolean indicating if it succeeded.
88
+ * It can only fail if there is no index to fulfill the request
89
+ * and the optimizedOnly option is set to true,
90
+ * or, the entire state was already loaded.
91
+ */
92
+ requestSnapshot(opts?: RequestSnapshotOptions): boolean {
93
+ if (this.loadedInitialState) {
94
+ // Subscription was deoptimized so we already sent the entire initial state
95
+ return false
96
+ }
97
+
98
+ const stateOpts: RequestSnapshotOptions = {
99
+ where: this.options.whereExpression,
100
+ optimizedOnly: opts?.optimizedOnly ?? false,
101
+ }
102
+
103
+ if (opts) {
104
+ if (`where` in opts) {
105
+ const snapshotWhereExp = opts.where
106
+ if (stateOpts.where) {
107
+ // Combine the two where expressions
108
+ const subWhereExp = stateOpts.where
109
+ const combinedWhereExp = and(subWhereExp, snapshotWhereExp)
110
+ stateOpts.where = combinedWhereExp
111
+ } else {
112
+ stateOpts.where = snapshotWhereExp
113
+ }
114
+ }
115
+ } else {
116
+ // No options provided so it's loading the entire initial state
117
+ this.loadedInitialState = true
118
+ }
119
+
120
+ const snapshot = this.collection.currentStateAsChanges(stateOpts)
121
+
122
+ if (snapshot === undefined) {
123
+ // Couldn't load from indexes
124
+ return false
125
+ }
126
+
127
+ // Only send changes that have not been sent yet
128
+ const filteredSnapshot = snapshot.filter(
129
+ (change) => !this.sentKeys.has(change.key)
130
+ )
131
+
132
+ this.snapshotSent = true
133
+ this.callback(filteredSnapshot)
134
+ return true
135
+ }
136
+
137
+ /**
138
+ * Sends a snapshot that is limited to the first `limit` rows that fulfill the `where` clause and are bigger than `minValue`.
139
+ * Requires a range index to be set with `setOrderByIndex` prior to calling this method.
140
+ * It uses that range index to load the items in the order of the index.
141
+ * Note: it does not send keys that have already been sent before.
142
+ */
143
+ requestLimitedSnapshot({ limit, minValue }: RequestLimitedSnapshotOptions) {
144
+ if (!limit) throw new Error(`limit is required`)
145
+
146
+ if (!this.orderByIndex) {
147
+ throw new Error(
148
+ `Ordered snapshot was requested but no index was found. You have to call setOrderByIndex before requesting an ordered snapshot.`
149
+ )
150
+ }
151
+
152
+ const index = this.orderByIndex
153
+ const where = this.options.whereExpression
154
+ const whereFilterFn = where
155
+ ? createFilterFunctionFromExpression(where)
156
+ : undefined
157
+
158
+ const filterFn = (key: string | number): boolean => {
159
+ if (this.sentKeys.has(key)) {
160
+ return false
161
+ }
162
+
163
+ const value = this.collection.get(key)
164
+ if (value === undefined) {
165
+ return false
166
+ }
167
+
168
+ return whereFilterFn?.(value) ?? true
169
+ }
170
+
171
+ let biggestObservedValue = minValue
172
+ const changes: Array<ChangeMessage<any, string | number>> = []
173
+ let keys: Array<string | number> = index.take(limit, minValue, filterFn)
174
+
175
+ const valuesNeeded = () => Math.max(limit - changes.length, 0)
176
+ const collectionExhausted = () => keys.length === 0
177
+
178
+ while (valuesNeeded() > 0 && !collectionExhausted()) {
179
+ for (const key of keys) {
180
+ const value = this.collection.get(key)!
181
+ changes.push({
182
+ type: `insert`,
183
+ key,
184
+ value,
185
+ })
186
+ biggestObservedValue = value
187
+ }
188
+
189
+ keys = index.take(valuesNeeded(), biggestObservedValue, filterFn)
190
+ }
191
+
192
+ this.callback(changes)
193
+ }
194
+
195
+ /**
196
+ * Filters and flips changes for keys that have not been sent yet.
197
+ * Deletes are filtered out for keys that have not been sent yet.
198
+ * Updates are flipped into inserts for keys that have not been sent yet.
199
+ */
200
+ private filterAndFlipChanges(changes: Array<ChangeMessage<any, any>>) {
201
+ if (this.loadedInitialState) {
202
+ // We loaded the entire initial state
203
+ // so no need to filter or flip changes
204
+ return changes
205
+ }
206
+
207
+ const newChanges = []
208
+ for (const change of changes) {
209
+ let newChange = change
210
+ if (!this.sentKeys.has(change.key)) {
211
+ if (change.type === `update`) {
212
+ newChange = { ...change, type: `insert`, previousValue: undefined }
213
+ } else if (change.type === `delete`) {
214
+ // filter out deletes for keys that have not been sent
215
+ continue
216
+ }
217
+ this.sentKeys.add(change.key)
218
+ }
219
+ newChanges.push(newChange)
220
+ }
221
+ return newChanges
222
+ }
223
+
224
+ private trackSentKeys(changes: Array<ChangeMessage<any, string | number>>) {
225
+ if (this.loadedInitialState) {
226
+ // No need to track sent keys if we loaded the entire state.
227
+ // Since we sent everything, all keys must have been observed.
228
+ return
229
+ }
230
+
231
+ for (const change of changes) {
232
+ this.sentKeys.add(change.key)
233
+ }
234
+ }
235
+
236
+ unsubscribe() {
237
+ this.options.onUnsubscribe?.()
238
+ }
239
+ }
@@ -0,0 +1,235 @@
1
+ import {
2
+ CollectionIsInErrorStateError,
3
+ DuplicateKeySyncError,
4
+ NoPendingSyncTransactionCommitError,
5
+ NoPendingSyncTransactionWriteError,
6
+ SyncCleanupError,
7
+ SyncTransactionAlreadyCommittedError,
8
+ SyncTransactionAlreadyCommittedWriteError,
9
+ } from "../errors"
10
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
11
+ import type { ChangeMessage, CollectionConfig } from "../types"
12
+ import type { CollectionImpl } from "./index.js"
13
+ import type { CollectionStateManager } from "./state"
14
+ import type { CollectionLifecycleManager } from "./lifecycle"
15
+
16
+ export class CollectionSyncManager<
17
+ TOutput extends object = Record<string, unknown>,
18
+ TKey extends string | number = string | number,
19
+ TSchema extends StandardSchemaV1 = StandardSchemaV1,
20
+ TInput extends object = TOutput,
21
+ > {
22
+ private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
23
+ private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>
24
+ private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
25
+ private config!: CollectionConfig<TOutput, TKey, TSchema>
26
+ private id: string
27
+
28
+ public preloadPromise: Promise<void> | null = null
29
+ public syncCleanupFn: (() => void) | null = null
30
+
31
+ /**
32
+ * Creates a new CollectionSyncManager instance
33
+ */
34
+ constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {
35
+ this.config = config
36
+ this.id = id
37
+ }
38
+
39
+ setDeps(deps: {
40
+ collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>
41
+ state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
42
+ lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
43
+ }) {
44
+ this.collection = deps.collection
45
+ this.state = deps.state
46
+ this.lifecycle = deps.lifecycle
47
+ }
48
+
49
+ /**
50
+ * Start the sync process for this collection
51
+ * This is called when the collection is first accessed or preloaded
52
+ */
53
+ public startSync(): void {
54
+ const state = this.state
55
+ if (
56
+ this.lifecycle.status !== `idle` &&
57
+ this.lifecycle.status !== `cleaned-up`
58
+ ) {
59
+ return // Already started or in progress
60
+ }
61
+
62
+ this.lifecycle.setStatus(`loading`)
63
+
64
+ try {
65
+ const cleanupFn = this.config.sync.sync({
66
+ collection: this.collection,
67
+ begin: () => {
68
+ state.pendingSyncedTransactions.push({
69
+ committed: false,
70
+ operations: [],
71
+ deletedKeys: new Set(),
72
+ })
73
+ },
74
+ write: (messageWithoutKey: Omit<ChangeMessage<TOutput>, `key`>) => {
75
+ const pendingTransaction =
76
+ state.pendingSyncedTransactions[
77
+ state.pendingSyncedTransactions.length - 1
78
+ ]
79
+ if (!pendingTransaction) {
80
+ throw new NoPendingSyncTransactionWriteError()
81
+ }
82
+ if (pendingTransaction.committed) {
83
+ throw new SyncTransactionAlreadyCommittedWriteError()
84
+ }
85
+ const key = this.config.getKey(messageWithoutKey.value)
86
+
87
+ // Check if an item with this key already exists when inserting
88
+ if (messageWithoutKey.type === `insert`) {
89
+ const insertingIntoExistingSynced = state.syncedData.has(key)
90
+ const hasPendingDeleteForKey =
91
+ pendingTransaction.deletedKeys.has(key)
92
+ const isTruncateTransaction = pendingTransaction.truncate === true
93
+ // Allow insert after truncate in the same transaction even if it existed in syncedData
94
+ if (
95
+ insertingIntoExistingSynced &&
96
+ !hasPendingDeleteForKey &&
97
+ !isTruncateTransaction
98
+ ) {
99
+ throw new DuplicateKeySyncError(key, this.id)
100
+ }
101
+ }
102
+
103
+ const message: ChangeMessage<TOutput> = {
104
+ ...messageWithoutKey,
105
+ key,
106
+ }
107
+ pendingTransaction.operations.push(message)
108
+
109
+ if (messageWithoutKey.type === `delete`) {
110
+ pendingTransaction.deletedKeys.add(key)
111
+ }
112
+ },
113
+ commit: () => {
114
+ const pendingTransaction =
115
+ state.pendingSyncedTransactions[
116
+ state.pendingSyncedTransactions.length - 1
117
+ ]
118
+ if (!pendingTransaction) {
119
+ throw new NoPendingSyncTransactionCommitError()
120
+ }
121
+ if (pendingTransaction.committed) {
122
+ throw new SyncTransactionAlreadyCommittedError()
123
+ }
124
+
125
+ pendingTransaction.committed = true
126
+
127
+ // Update status to initialCommit when transitioning from loading
128
+ // This indicates we're in the process of committing the first transaction
129
+ if (this.lifecycle.status === `loading`) {
130
+ this.lifecycle.setStatus(`initialCommit`)
131
+ }
132
+
133
+ state.commitPendingTransactions()
134
+ },
135
+ markReady: () => {
136
+ this.lifecycle.markReady()
137
+ },
138
+ truncate: () => {
139
+ const pendingTransaction =
140
+ state.pendingSyncedTransactions[
141
+ state.pendingSyncedTransactions.length - 1
142
+ ]
143
+ if (!pendingTransaction) {
144
+ throw new NoPendingSyncTransactionWriteError()
145
+ }
146
+ if (pendingTransaction.committed) {
147
+ throw new SyncTransactionAlreadyCommittedWriteError()
148
+ }
149
+
150
+ // Clear all operations from the current transaction
151
+ pendingTransaction.operations = []
152
+ pendingTransaction.deletedKeys.clear()
153
+
154
+ // Mark the transaction as a truncate operation. During commit, this triggers:
155
+ // - Delete events for all previously synced keys (excluding optimistic-deleted keys)
156
+ // - Clearing of syncedData/syncedMetadata
157
+ // - Subsequent synced ops applied on the fresh base
158
+ // - Finally, optimistic mutations re-applied on top (single batch)
159
+ pendingTransaction.truncate = true
160
+ },
161
+ })
162
+
163
+ // Store cleanup function if provided
164
+ this.syncCleanupFn = typeof cleanupFn === `function` ? cleanupFn : null
165
+ } catch (error) {
166
+ this.lifecycle.setStatus(`error`)
167
+ throw error
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Preload the collection data by starting sync if not already started
173
+ * Multiple concurrent calls will share the same promise
174
+ */
175
+ public preload(): Promise<void> {
176
+ if (this.preloadPromise) {
177
+ return this.preloadPromise
178
+ }
179
+
180
+ this.preloadPromise = new Promise<void>((resolve, reject) => {
181
+ if (this.lifecycle.status === `ready`) {
182
+ resolve()
183
+ return
184
+ }
185
+
186
+ if (this.lifecycle.status === `error`) {
187
+ reject(new CollectionIsInErrorStateError())
188
+ return
189
+ }
190
+
191
+ // Register callback BEFORE starting sync to avoid race condition
192
+ this.lifecycle.onFirstReady(() => {
193
+ resolve()
194
+ })
195
+
196
+ // Start sync if collection hasn't started yet or was cleaned up
197
+ if (
198
+ this.lifecycle.status === `idle` ||
199
+ this.lifecycle.status === `cleaned-up`
200
+ ) {
201
+ try {
202
+ this.startSync()
203
+ } catch (error) {
204
+ reject(error)
205
+ return
206
+ }
207
+ }
208
+ })
209
+
210
+ return this.preloadPromise
211
+ }
212
+
213
+ public cleanup(): void {
214
+ try {
215
+ if (this.syncCleanupFn) {
216
+ this.syncCleanupFn()
217
+ this.syncCleanupFn = null
218
+ }
219
+ } catch (error) {
220
+ // Re-throw in a microtask to surface the error after cleanup completes
221
+ queueMicrotask(() => {
222
+ if (error instanceof Error) {
223
+ // Preserve the original error and stack trace
224
+ const wrappedError = new SyncCleanupError(this.id, error)
225
+ wrappedError.cause = error
226
+ wrappedError.stack = error.stack
227
+ throw wrappedError
228
+ } else {
229
+ throw new SyncCleanupError(this.id, error as Error | string)
230
+ }
231
+ })
232
+ }
233
+ this.preloadPromise = null
234
+ }
235
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // Re-export all public APIs
2
- export * from "./collection"
2
+ export * from "./collection/index.js"
3
3
  export * from "./SortedMap"
4
4
  export * from "./transactions"
5
5
  export * from "./types"
@@ -17,4 +17,4 @@ export * from "./indexes/lazy-index.js"
17
17
  export { type IndexOptions } from "./indexes/index-options.js"
18
18
 
19
19
  // Re-export some stuff explicitly to ensure the type & value is exported
20
- export type { Collection } from "./collection"
20
+ export type { Collection } from "./collection/index.js"
@@ -1,6 +1,6 @@
1
1
  import { BTreeIndex } from "./btree-index"
2
2
  import type { BasicExpression } from "../query/ir"
3
- import type { CollectionImpl } from "../collection"
3
+ import type { CollectionImpl } from "../collection/index.js"
4
4
 
5
5
  export interface AutoIndexConfig {
6
6
  autoIndex?: `off` | `eager`
@@ -1,6 +1,6 @@
1
1
  import { compileSingleRowExpression } from "../query/compiler/evaluators.js"
2
2
  import { comparisonFunctions } from "../query/builder/functions.js"
3
- import type { BasicExpression, OrderByDirection } from "../query/ir.js"
3
+ import type { BasicExpression } from "../query/ir.js"
4
4
 
5
5
  /**
6
6
  * Operations that indexes can support, imported from available comparison functions
@@ -58,8 +58,8 @@ export abstract class BaseIndex<
58
58
  abstract lookup(operation: IndexOperation, value: any): Set<TKey>
59
59
  abstract take(
60
60
  n: number,
61
- direction?: OrderByDirection,
62
- from?: TKey
61
+ from?: TKey,
62
+ filterFn?: (key: TKey) => boolean
63
63
  ): Array<TKey>
64
64
  abstract get keyCount(): number
65
65
 
@@ -236,7 +236,7 @@ export class BTreeIndex<
236
236
  * @param from - The item to start from (exclusive). Starts from the smallest item (inclusive) if not provided.
237
237
  * @returns The next n items after the provided key. Returns the first n items if no from item is provided.
238
238
  */
239
- take(n: number, from?: any): Array<TKey> {
239
+ take(n: number, from?: any, filterFn?: (key: TKey) => boolean): Array<TKey> {
240
240
  const keysInResult: Set<TKey> = new Set()
241
241
  const result: Array<TKey> = []
242
242
  const nextKey = (k?: any) => this.orderedEntries.nextHigherKey(k)
@@ -248,7 +248,7 @@ export class BTreeIndex<
248
248
  const it = keys.values()
249
249
  let ks: TKey | undefined
250
250
  while (result.length < n && (ks = it.next().value)) {
251
- if (!keysInResult.has(ks)) {
251
+ if (!keysInResult.has(ks) && (filterFn?.(ks) ?? true)) {
252
252
  result.push(ks)
253
253
  keysInResult.add(ks)
254
254
  }
@@ -1,4 +1,4 @@
1
- import { CollectionImpl } from "../../collection.js"
1
+ import { CollectionImpl } from "../../collection/index.js"
2
2
  import {
3
3
  Aggregate as AggregateExpr,
4
4
  CollectionRef,
@@ -1,4 +1,4 @@
1
- import type { CollectionImpl } from "../../collection.js"
1
+ import type { CollectionImpl } from "../../collection/index.js"
2
2
  import type {
3
3
  Aggregate,
4
4
  BasicExpression,
@@ -13,6 +13,7 @@ import { processJoins } from "./joins.js"
13
13
  import { processGroupBy } from "./group-by.js"
14
14
  import { processOrderBy } from "./order-by.js"
15
15
  import { processSelect } from "./select.js"
16
+ import type { CollectionSubscription } from "../../collection/subscription.js"
16
17
  import type { OrderByOptimizationInfo } from "./order-by.js"
17
18
  import type {
18
19
  BasicExpression,
@@ -21,7 +22,7 @@ import type {
21
22
  QueryRef,
22
23
  } from "../ir.js"
23
24
  import type { LazyCollectionCallbacks } from "./joins.js"
24
- import type { Collection } from "../../collection.js"
25
+ import type { Collection } from "../../collection/index.js"
25
26
  import type {
26
27
  KeyedStream,
27
28
  NamespacedAndKeyedStream,
@@ -53,6 +54,7 @@ export function compileQuery(
53
54
  rawQuery: QueryIR,
54
55
  inputs: Record<string, KeyedStream>,
55
56
  collections: Record<string, Collection<any, any, any, any, any>>,
57
+ subscriptions: Record<string, CollectionSubscription>,
56
58
  callbacks: Record<string, LazyCollectionCallbacks>,
57
59
  lazyCollections: Set<string>,
58
60
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
@@ -88,6 +90,7 @@ export function compileQuery(
88
90
  query.from,
89
91
  allInputs,
90
92
  collections,
93
+ subscriptions,
91
94
  callbacks,
92
95
  lazyCollections,
93
96
  optimizableOrderByCollections,
@@ -120,6 +123,7 @@ export function compileQuery(
120
123
  cache,
121
124
  queryMapping,
122
125
  collections,
126
+ subscriptions,
123
127
  callbacks,
124
128
  lazyCollections,
125
129
  optimizableOrderByCollections,
@@ -323,6 +327,7 @@ function processFrom(
323
327
  from: CollectionRef | QueryRef,
324
328
  allInputs: Record<string, KeyedStream>,
325
329
  collections: Record<string, Collection>,
330
+ subscriptions: Record<string, CollectionSubscription>,
326
331
  callbacks: Record<string, LazyCollectionCallbacks>,
327
332
  lazyCollections: Set<string>,
328
333
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
@@ -346,6 +351,7 @@ function processFrom(
346
351
  originalQuery,
347
352
  allInputs,
348
353
  collections,
354
+ subscriptions,
349
355
  callbacks,
350
356
  lazyCollections,
351
357
  optimizableOrderByCollections,