@tanstack/db 0.3.1 → 0.4.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.
- package/dist/cjs/{change-events.cjs → collection/change-events.cjs} +13 -42
- package/dist/cjs/collection/change-events.cjs.map +1 -0
- package/dist/{esm/change-events.d.ts → cjs/collection/change-events.d.cts} +6 -6
- package/dist/cjs/collection/changes.cjs +108 -0
- package/dist/cjs/collection/changes.cjs.map +1 -0
- package/dist/cjs/collection/changes.d.cts +53 -0
- package/dist/cjs/collection/events.cjs +90 -0
- package/dist/cjs/collection/events.cjs.map +1 -0
- package/dist/cjs/collection/events.d.cts +53 -0
- package/dist/cjs/collection/index.cjs +417 -0
- package/dist/cjs/collection/index.cjs.map +1 -0
- package/dist/{esm/collection.d.ts → cjs/collection/index.d.cts} +56 -172
- package/dist/cjs/collection/indexes.cjs +124 -0
- package/dist/cjs/collection/indexes.cjs.map +1 -0
- package/dist/cjs/collection/indexes.d.cts +47 -0
- package/dist/cjs/collection/lifecycle.cjs +150 -0
- package/dist/cjs/collection/lifecycle.cjs.map +1 -0
- package/dist/cjs/collection/lifecycle.d.cts +70 -0
- package/dist/cjs/collection/mutations.cjs +315 -0
- package/dist/cjs/collection/mutations.cjs.map +1 -0
- package/dist/cjs/collection/mutations.d.cts +33 -0
- package/dist/cjs/collection/state.cjs +597 -0
- package/dist/cjs/collection/state.cjs.map +1 -0
- package/dist/cjs/collection/state.d.cts +122 -0
- package/dist/cjs/collection/subscription.cjs +160 -0
- package/dist/cjs/collection/subscription.cjs.map +1 -0
- package/dist/cjs/collection/subscription.d.cts +57 -0
- package/dist/cjs/collection/sync.cjs +154 -0
- package/dist/cjs/collection/sync.cjs.map +1 -0
- package/dist/cjs/collection/sync.d.cts +34 -0
- package/dist/cjs/index.cjs +8 -8
- package/dist/cjs/index.d.cts +2 -2
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/auto-index.d.cts +1 -1
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -2
- package/dist/cjs/indexes/btree-index.cjs +2 -2
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +1 -1
- package/dist/cjs/query/builder/index.cjs +2 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +1 -1
- package/dist/cjs/query/compiler/index.cjs +5 -2
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +3 -2
- package/dist/cjs/query/compiler/joins.cjs +22 -24
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +3 -2
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +29 -12
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +3 -0
- package/dist/cjs/query/live/collection-subscriber.cjs +43 -104
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
- package/dist/cjs/query/live-query-collection.cjs +2 -2
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +1 -1
- package/dist/cjs/transactions.cjs +3 -3
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +12 -10
- package/dist/{cjs/change-events.d.cts → esm/collection/change-events.d.ts} +6 -6
- package/dist/esm/{change-events.js → collection/change-events.js} +13 -42
- package/dist/esm/collection/change-events.js.map +1 -0
- package/dist/esm/collection/changes.d.ts +53 -0
- package/dist/esm/collection/changes.js +108 -0
- package/dist/esm/collection/changes.js.map +1 -0
- package/dist/esm/collection/events.d.ts +53 -0
- package/dist/esm/collection/events.js +90 -0
- package/dist/esm/collection/events.js.map +1 -0
- package/dist/{cjs/collection.d.cts → esm/collection/index.d.ts} +56 -172
- package/dist/esm/collection/index.js +417 -0
- package/dist/esm/collection/index.js.map +1 -0
- package/dist/esm/collection/indexes.d.ts +47 -0
- package/dist/esm/collection/indexes.js +124 -0
- package/dist/esm/collection/indexes.js.map +1 -0
- package/dist/esm/collection/lifecycle.d.ts +70 -0
- package/dist/esm/collection/lifecycle.js +150 -0
- package/dist/esm/collection/lifecycle.js.map +1 -0
- package/dist/esm/collection/mutations.d.ts +33 -0
- package/dist/esm/collection/mutations.js +315 -0
- package/dist/esm/collection/mutations.js.map +1 -0
- package/dist/esm/collection/state.d.ts +122 -0
- package/dist/esm/collection/state.js +597 -0
- package/dist/esm/collection/state.js.map +1 -0
- package/dist/esm/collection/subscription.d.ts +57 -0
- package/dist/esm/collection/subscription.js +160 -0
- package/dist/esm/collection/subscription.js.map +1 -0
- package/dist/esm/collection/sync.d.ts +34 -0
- package/dist/esm/collection/sync.js +154 -0
- package/dist/esm/collection/sync.js.map +1 -0
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -1
- package/dist/esm/indexes/auto-index.d.ts +1 -1
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -2
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/btree-index.d.ts +1 -1
- package/dist/esm/indexes/btree-index.js +2 -2
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/proxy.js +1 -1
- package/dist/esm/query/builder/index.js +1 -1
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +1 -1
- package/dist/esm/query/compiler/index.d.ts +3 -2
- package/dist/esm/query/compiler/index.js +5 -2
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +3 -2
- package/dist/esm/query/compiler/joins.js +22 -24
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -1
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/ir.d.ts +1 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +3 -0
- package/dist/esm/query/live/collection-config-builder.js +29 -12
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
- package/dist/esm/query/live/collection-subscriber.js +43 -104
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +1 -1
- package/dist/esm/query/live-query-collection.js +1 -1
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/transactions.js +3 -3
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +12 -10
- package/package.json +2 -2
- package/src/{change-events.ts → collection/change-events.ts} +25 -39
- package/src/collection/changes.ts +163 -0
- package/src/collection/events.ts +171 -0
- package/src/collection/index.ts +808 -0
- package/src/collection/indexes.ts +172 -0
- package/src/collection/lifecycle.ts +221 -0
- package/src/collection/mutations.ts +535 -0
- package/src/collection/state.ts +866 -0
- package/src/collection/subscription.ts +239 -0
- package/src/collection/sync.ts +235 -0
- package/src/index.ts +2 -2
- package/src/indexes/auto-index.ts +1 -1
- package/src/indexes/base-index.ts +3 -3
- package/src/indexes/btree-index.ts +2 -2
- package/src/query/builder/index.ts +1 -1
- package/src/query/builder/types.ts +1 -1
- package/src/query/compiler/index.ts +7 -1
- package/src/query/compiler/joins.ts +28 -41
- package/src/query/compiler/order-by.ts +1 -1
- package/src/query/ir.ts +1 -1
- package/src/query/live/collection-config-builder.ts +48 -22
- package/src/query/live/collection-subscriber.ts +63 -168
- package/src/query/live-query-collection.ts +2 -2
- package/src/transactions.ts +3 -3
- package/src/types.ts +14 -15
- package/dist/cjs/change-events.cjs.map +0 -1
- package/dist/cjs/collection.cjs +0 -1580
- package/dist/cjs/collection.cjs.map +0 -1
- package/dist/esm/change-events.js.map +0 -1
- package/dist/esm/collection.js +0 -1580
- package/dist/esm/collection.js.map +0 -1
- package/src/collection.ts +0 -2488
|
@@ -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
|
|
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
|
-
|
|
62
|
-
|
|
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
|
}
|
|
@@ -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,
|