11import { ensureIndexForExpression } from "../indexes/auto-index.js"
2- import { and , gt , lt } from "../query/builder/functions.js"
2+ import { and , eq , gt , lt } from "../query/builder/functions.js"
33import { Value } from "../query/ir.js"
44import { EventEmitter } from "../event-emitter.js"
55import {
@@ -20,6 +20,7 @@ import type { CollectionImpl } from "./index.js"
2020type RequestSnapshotOptions = {
2121 where ?: BasicExpression < boolean >
2222 optimizedOnly ?: boolean
23+ trackLoadSubsetPromise ?: boolean
2324}
2425
2526type RequestLimitedSnapshotOptions = {
@@ -197,7 +198,10 @@ export class CollectionSubscription
197198 subscription : this ,
198199 } )
199200
200- this . trackLoadSubsetPromise ( syncResult )
201+ const trackLoadSubsetPromise = opts ?. trackLoadSubsetPromise ?? true
202+ if ( trackLoadSubsetPromise ) {
203+ this . trackLoadSubsetPromise ( syncResult )
204+ }
201205
202206 // Also load data immediately from the collection
203207 const snapshot = this . collection . currentStateAsChanges ( stateOpts )
@@ -218,10 +222,12 @@ export class CollectionSubscription
218222 }
219223
220224 /**
221- * Sends a snapshot that is limited to the first `limit` rows that fulfill the ` where` clause and are bigger than `minValue`.
225+ * Sends a snapshot that fulfills the ` where` clause and all rows are bigger or equal to `minValue`.
222226 * Requires a range index to be set with `setOrderByIndex` prior to calling this method.
223227 * It uses that range index to load the items in the order of the index.
224- * Note: it does not send keys that have already been sent before.
228+ * Note 1: it may load more rows than the provided LIMIT because it loads all values equal to `minValue` + limit values greater than `minValue`.
229+ * This is needed to ensure that it does not accidentally skip duplicate values when the limit falls in the middle of some duplicated values.
230+ * Note 2: it does not send keys that have already been sent before.
225231 */
226232 requestLimitedSnapshot ( {
227233 orderBy,
@@ -257,12 +263,49 @@ export class CollectionSubscription
257263
258264 let biggestObservedValue = minValue
259265 const changes : Array < ChangeMessage < any , string | number > > = [ ]
260- let keys : Array < string | number > = index . take ( limit , minValue , filterFn )
266+
267+ // If we have a minValue we need to handle the case
268+ // where there might be duplicate values equal to minValue that we need to include
269+ // because we can have data like this: [1, 2, 3, 3, 3, 4, 5]
270+ // so if minValue is 3 then the previous snapshot may not have included all 3s
271+ // e.g. if it was offset 0 and limit 3 it would only have loaded the first 3
272+ // so we load all rows equal to minValue first, to be sure we don't skip any duplicate values
273+ let keys : Array < string | number > = [ ]
274+ if ( minValue !== undefined ) {
275+ // First, get all items with the same value as minValue
276+ const { expression } = orderBy [ 0 ] !
277+ const allRowsWithMinValue = this . collection . currentStateAsChanges ( {
278+ where : eq ( expression , new Value ( minValue ) ) ,
279+ } )
280+
281+ if ( allRowsWithMinValue ) {
282+ const keysWithMinValue = allRowsWithMinValue
283+ . map ( ( change ) => change . key )
284+ . filter ( ( key ) => ! this . sentKeys . has ( key ) && filterFn ( key ) )
285+
286+ // Add items with the minValue first
287+ keys . push ( ...keysWithMinValue )
288+
289+ // Then get items greater than minValue
290+ const keysGreaterThanMin = index . take (
291+ limit - keys . length ,
292+ minValue ,
293+ filterFn
294+ )
295+ keys . push ( ...keysGreaterThanMin )
296+ } else {
297+ keys = index . take ( limit , minValue , filterFn )
298+ }
299+ } else {
300+ keys = index . take ( limit , minValue , filterFn )
301+ }
261302
262303 const valuesNeeded = ( ) => Math . max ( limit - changes . length , 0 )
263304 const collectionExhausted = ( ) => keys . length === 0
264305
265306 while ( valuesNeeded ( ) > 0 && ! collectionExhausted ( ) ) {
307+ const insertedKeys = new Set < string | number > ( ) // Track keys we add to `changes` in this iteration
308+
266309 for ( const key of keys ) {
267310 const value = this . collection . get ( key ) !
268311 changes . push ( {
@@ -271,6 +314,7 @@ export class CollectionSubscription
271314 value,
272315 } )
273316 biggestObservedValue = value
317+ insertedKeys . add ( key ) // Track this key
274318 }
275319
276320 keys = index . take ( valuesNeeded ( ) , biggestObservedValue , filterFn )
@@ -296,9 +340,41 @@ export class CollectionSubscription
296340 subscription : this ,
297341 } )
298342
299- this . trackLoadSubsetPromise ( syncResult )
343+ // Make parallel loadSubset calls for values equal to minValue and values greater than minValue
344+ const promises : Array < Promise < void > > = [ ]
345+
346+ // First promise: load all values equal to minValue
347+ if ( typeof minValue !== `undefined` ) {
348+ const { expression } = orderBy [ 0 ] !
349+ const exactValueFilter = eq ( expression , new Value ( minValue ) )
350+
351+ const equalValueResult = this . collection . _sync . loadSubset ( {
352+ where : exactValueFilter ,
353+ subscription : this ,
354+ } )
355+
356+ if ( equalValueResult instanceof Promise ) {
357+ promises . push ( equalValueResult )
358+ }
359+ }
360+
361+ // Second promise: load values greater than minValue
362+ if ( syncResult instanceof Promise ) {
363+ promises . push ( syncResult )
364+ }
365+
366+ // Track the combined promise
367+ if ( promises . length > 0 ) {
368+ const combinedPromise = Promise . all ( promises ) . then ( ( ) => { } )
369+ this . trackLoadSubsetPromise ( combinedPromise )
370+ } else {
371+ this . trackLoadSubsetPromise ( syncResult )
372+ }
300373 }
301374
375+ // TODO: also add similar test but that checks that it can also load it from the collection's loadSubset function
376+ // and that that also works properly (i.e. does not skip duplicate values)
377+
302378 /**
303379 * Filters and flips changes for keys that have not been sent yet.
304380 * Deletes are filtered out for keys that have not been sent yet.
0 commit comments