@tanstack/db 0.4.4 → 0.4.6

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 (75) hide show
  1. package/dist/cjs/collection/change-events.cjs +1 -1
  2. package/dist/cjs/collection/change-events.cjs.map +1 -1
  3. package/dist/cjs/collection/change-events.d.cts +1 -1
  4. package/dist/cjs/collection/index.cjs +11 -0
  5. package/dist/cjs/collection/index.cjs.map +1 -1
  6. package/dist/cjs/collection/index.d.cts +8 -1
  7. package/dist/cjs/collection/lifecycle.cjs +4 -1
  8. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  9. package/dist/cjs/collection/state.cjs +5 -4
  10. package/dist/cjs/collection/state.cjs.map +1 -1
  11. package/dist/cjs/collection/subscription.cjs +21 -1
  12. package/dist/cjs/collection/subscription.cjs.map +1 -1
  13. package/dist/cjs/collection/subscription.d.cts +4 -3
  14. package/dist/cjs/collection/sync.cjs +94 -71
  15. package/dist/cjs/collection/sync.cjs.map +1 -1
  16. package/dist/cjs/collection/sync.d.cts +9 -1
  17. package/dist/cjs/index.cjs +2 -0
  18. package/dist/cjs/index.cjs.map +1 -1
  19. package/dist/cjs/index.d.cts +2 -0
  20. package/dist/cjs/indexes/auto-index.cjs +4 -1
  21. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  22. package/dist/cjs/query/compiler/expressions.cjs +19 -0
  23. package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
  24. package/dist/cjs/query/compiler/expressions.d.cts +2 -1
  25. package/dist/cjs/query/compiler/order-by.cjs +2 -1
  26. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  27. package/dist/cjs/query/compiler/order-by.d.cts +2 -1
  28. package/dist/cjs/query/live/collection-subscriber.cjs +18 -8
  29. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  30. package/dist/cjs/query/live/collection-subscriber.d.cts +1 -0
  31. package/dist/cjs/types.d.cts +11 -1
  32. package/dist/esm/collection/change-events.d.ts +1 -1
  33. package/dist/esm/collection/change-events.js +1 -1
  34. package/dist/esm/collection/change-events.js.map +1 -1
  35. package/dist/esm/collection/index.d.ts +8 -1
  36. package/dist/esm/collection/index.js +11 -0
  37. package/dist/esm/collection/index.js.map +1 -1
  38. package/dist/esm/collection/lifecycle.js +4 -1
  39. package/dist/esm/collection/lifecycle.js.map +1 -1
  40. package/dist/esm/collection/state.js +5 -4
  41. package/dist/esm/collection/state.js.map +1 -1
  42. package/dist/esm/collection/subscription.d.ts +4 -3
  43. package/dist/esm/collection/subscription.js +22 -2
  44. package/dist/esm/collection/subscription.js.map +1 -1
  45. package/dist/esm/collection/sync.d.ts +9 -1
  46. package/dist/esm/collection/sync.js +94 -71
  47. package/dist/esm/collection/sync.js.map +1 -1
  48. package/dist/esm/index.d.ts +2 -0
  49. package/dist/esm/index.js +2 -0
  50. package/dist/esm/index.js.map +1 -1
  51. package/dist/esm/indexes/auto-index.js +4 -1
  52. package/dist/esm/indexes/auto-index.js.map +1 -1
  53. package/dist/esm/query/compiler/expressions.d.ts +2 -1
  54. package/dist/esm/query/compiler/expressions.js +19 -0
  55. package/dist/esm/query/compiler/expressions.js.map +1 -1
  56. package/dist/esm/query/compiler/order-by.d.ts +2 -1
  57. package/dist/esm/query/compiler/order-by.js +2 -1
  58. package/dist/esm/query/compiler/order-by.js.map +1 -1
  59. package/dist/esm/query/live/collection-subscriber.d.ts +1 -0
  60. package/dist/esm/query/live/collection-subscriber.js +19 -9
  61. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  62. package/dist/esm/types.d.ts +11 -1
  63. package/package.json +1 -1
  64. package/src/collection/change-events.ts +5 -2
  65. package/src/collection/index.ts +13 -0
  66. package/src/collection/lifecycle.ts +4 -1
  67. package/src/collection/state.ts +6 -6
  68. package/src/collection/subscription.ts +34 -4
  69. package/src/collection/sync.ts +147 -110
  70. package/src/index.ts +5 -0
  71. package/src/indexes/auto-index.ts +4 -1
  72. package/src/query/compiler/expressions.ts +26 -1
  73. package/src/query/compiler/order-by.ts +3 -1
  74. package/src/query/live/collection-subscriber.ts +31 -10
  75. package/src/types.ts +13 -1
@@ -9,7 +9,13 @@ import {
9
9
  } from "../errors"
10
10
  import { deepEquals } from "../utils"
11
11
  import type { StandardSchemaV1 } from "@standard-schema/spec"
12
- import type { ChangeMessage, CollectionConfig } from "../types"
12
+ import type {
13
+ ChangeMessage,
14
+ CleanupFn,
15
+ CollectionConfig,
16
+ OnLoadMoreOptions,
17
+ SyncConfigRes,
18
+ } from "../types"
13
19
  import type { CollectionImpl } from "./index.js"
14
20
  import type { CollectionStateManager } from "./state"
15
21
  import type { CollectionLifecycleManager } from "./lifecycle"
@@ -28,6 +34,9 @@ export class CollectionSyncManager<
28
34
 
29
35
  public preloadPromise: Promise<void> | null = null
30
36
  public syncCleanupFn: (() => void) | null = null
37
+ public syncOnLoadMoreFn:
38
+ | ((options: OnLoadMoreOptions) => void | Promise<void>)
39
+ | null = null
31
40
 
32
41
  /**
33
42
  * Creates a new CollectionSyncManager instance
@@ -52,7 +61,6 @@ export class CollectionSyncManager<
52
61
  * This is called when the collection is first accessed or preloaded
53
62
  */
54
63
  public startSync(): void {
55
- const state = this.state
56
64
  if (
57
65
  this.lifecycle.status !== `idle` &&
58
66
  this.lifecycle.status !== `cleaned-up`
@@ -63,120 +71,125 @@ export class CollectionSyncManager<
63
71
  this.lifecycle.setStatus(`loading`)
64
72
 
65
73
  try {
66
- const cleanupFn = this.config.sync.sync({
67
- collection: this.collection,
68
- begin: () => {
69
- state.pendingSyncedTransactions.push({
70
- committed: false,
71
- operations: [],
72
- deletedKeys: new Set(),
73
- })
74
- },
75
- write: (messageWithoutKey: Omit<ChangeMessage<TOutput>, `key`>) => {
76
- const pendingTransaction =
77
- state.pendingSyncedTransactions[
78
- state.pendingSyncedTransactions.length - 1
79
- ]
80
- if (!pendingTransaction) {
81
- throw new NoPendingSyncTransactionWriteError()
82
- }
83
- if (pendingTransaction.committed) {
84
- throw new SyncTransactionAlreadyCommittedWriteError()
85
- }
86
- const key = this.config.getKey(messageWithoutKey.value)
87
-
88
- let messageType = messageWithoutKey.type
89
-
90
- // Check if an item with this key already exists when inserting
91
- if (messageWithoutKey.type === `insert`) {
92
- const insertingIntoExistingSynced = state.syncedData.has(key)
93
- const hasPendingDeleteForKey =
94
- pendingTransaction.deletedKeys.has(key)
95
- const isTruncateTransaction = pendingTransaction.truncate === true
96
- // Allow insert after truncate in the same transaction even if it existed in syncedData
97
- if (
98
- insertingIntoExistingSynced &&
99
- !hasPendingDeleteForKey &&
100
- !isTruncateTransaction
101
- ) {
102
- const existingValue = state.syncedData.get(key)
74
+ const syncRes = normalizeSyncFnResult(
75
+ this.config.sync.sync({
76
+ collection: this.collection,
77
+ begin: () => {
78
+ this.state.pendingSyncedTransactions.push({
79
+ committed: false,
80
+ operations: [],
81
+ deletedKeys: new Set(),
82
+ })
83
+ },
84
+ write: (messageWithoutKey: Omit<ChangeMessage<TOutput>, `key`>) => {
85
+ const pendingTransaction =
86
+ this.state.pendingSyncedTransactions[
87
+ this.state.pendingSyncedTransactions.length - 1
88
+ ]
89
+ if (!pendingTransaction) {
90
+ throw new NoPendingSyncTransactionWriteError()
91
+ }
92
+ if (pendingTransaction.committed) {
93
+ throw new SyncTransactionAlreadyCommittedWriteError()
94
+ }
95
+ const key = this.config.getKey(messageWithoutKey.value)
96
+
97
+ let messageType = messageWithoutKey.type
98
+
99
+ // Check if an item with this key already exists when inserting
100
+ if (messageWithoutKey.type === `insert`) {
101
+ const insertingIntoExistingSynced = this.state.syncedData.has(key)
102
+ const hasPendingDeleteForKey =
103
+ pendingTransaction.deletedKeys.has(key)
104
+ const isTruncateTransaction = pendingTransaction.truncate === true
105
+ // Allow insert after truncate in the same transaction even if it existed in syncedData
103
106
  if (
104
- existingValue !== undefined &&
105
- deepEquals(existingValue, messageWithoutKey.value)
107
+ insertingIntoExistingSynced &&
108
+ !hasPendingDeleteForKey &&
109
+ !isTruncateTransaction
106
110
  ) {
107
- // The "insert" is an echo of a value we already have locally.
108
- // Treat it as an update so we preserve optimistic intent without
109
- // throwing a duplicate-key error during reconciliation.
110
- messageType = `update`
111
- } else {
112
- throw new DuplicateKeySyncError(key, this.id)
111
+ const existingValue = this.state.syncedData.get(key)
112
+ if (
113
+ existingValue !== undefined &&
114
+ deepEquals(existingValue, messageWithoutKey.value)
115
+ ) {
116
+ // The "insert" is an echo of a value we already have locally.
117
+ // Treat it as an update so we preserve optimistic intent without
118
+ // throwing a duplicate-key error during reconciliation.
119
+ messageType = `update`
120
+ } else {
121
+ throw new DuplicateKeySyncError(key, this.id)
122
+ }
113
123
  }
114
124
  }
115
- }
116
-
117
- const message: ChangeMessage<TOutput> = {
118
- ...messageWithoutKey,
119
- type: messageType,
120
- key,
121
- }
122
- pendingTransaction.operations.push(message)
123
-
124
- if (messageType === `delete`) {
125
- pendingTransaction.deletedKeys.add(key)
126
- }
127
- },
128
- commit: () => {
129
- const pendingTransaction =
130
- state.pendingSyncedTransactions[
131
- state.pendingSyncedTransactions.length - 1
132
- ]
133
- if (!pendingTransaction) {
134
- throw new NoPendingSyncTransactionCommitError()
135
- }
136
- if (pendingTransaction.committed) {
137
- throw new SyncTransactionAlreadyCommittedError()
138
- }
139
-
140
- pendingTransaction.committed = true
141
-
142
- // Update status to initialCommit when transitioning from loading
143
- // This indicates we're in the process of committing the first transaction
144
- if (this.lifecycle.status === `loading`) {
145
- this.lifecycle.setStatus(`initialCommit`)
146
- }
147
-
148
- state.commitPendingTransactions()
149
- },
150
- markReady: () => {
151
- this.lifecycle.markReady()
152
- },
153
- truncate: () => {
154
- const pendingTransaction =
155
- state.pendingSyncedTransactions[
156
- state.pendingSyncedTransactions.length - 1
157
- ]
158
- if (!pendingTransaction) {
159
- throw new NoPendingSyncTransactionWriteError()
160
- }
161
- if (pendingTransaction.committed) {
162
- throw new SyncTransactionAlreadyCommittedWriteError()
163
- }
164
-
165
- // Clear all operations from the current transaction
166
- pendingTransaction.operations = []
167
- pendingTransaction.deletedKeys.clear()
168
-
169
- // Mark the transaction as a truncate operation. During commit, this triggers:
170
- // - Delete events for all previously synced keys (excluding optimistic-deleted keys)
171
- // - Clearing of syncedData/syncedMetadata
172
- // - Subsequent synced ops applied on the fresh base
173
- // - Finally, optimistic mutations re-applied on top (single batch)
174
- pendingTransaction.truncate = true
175
- },
176
- })
125
+
126
+ const message: ChangeMessage<TOutput> = {
127
+ ...messageWithoutKey,
128
+ type: messageType,
129
+ key,
130
+ }
131
+ pendingTransaction.operations.push(message)
132
+
133
+ if (messageType === `delete`) {
134
+ pendingTransaction.deletedKeys.add(key)
135
+ }
136
+ },
137
+ commit: () => {
138
+ const pendingTransaction =
139
+ this.state.pendingSyncedTransactions[
140
+ this.state.pendingSyncedTransactions.length - 1
141
+ ]
142
+ if (!pendingTransaction) {
143
+ throw new NoPendingSyncTransactionCommitError()
144
+ }
145
+ if (pendingTransaction.committed) {
146
+ throw new SyncTransactionAlreadyCommittedError()
147
+ }
148
+
149
+ pendingTransaction.committed = true
150
+
151
+ // Update status to initialCommit when transitioning from loading
152
+ // This indicates we're in the process of committing the first transaction
153
+ if (this.lifecycle.status === `loading`) {
154
+ this.lifecycle.setStatus(`initialCommit`)
155
+ }
156
+
157
+ this.state.commitPendingTransactions()
158
+ },
159
+ markReady: () => {
160
+ this.lifecycle.markReady()
161
+ },
162
+ truncate: () => {
163
+ const pendingTransaction =
164
+ this.state.pendingSyncedTransactions[
165
+ this.state.pendingSyncedTransactions.length - 1
166
+ ]
167
+ if (!pendingTransaction) {
168
+ throw new NoPendingSyncTransactionWriteError()
169
+ }
170
+ if (pendingTransaction.committed) {
171
+ throw new SyncTransactionAlreadyCommittedWriteError()
172
+ }
173
+
174
+ // Clear all operations from the current transaction
175
+ pendingTransaction.operations = []
176
+ pendingTransaction.deletedKeys.clear()
177
+
178
+ // Mark the transaction as a truncate operation. During commit, this triggers:
179
+ // - Delete events for all previously synced keys (excluding optimistic-deleted keys)
180
+ // - Clearing of syncedData/syncedMetadata
181
+ // - Subsequent synced ops applied on the fresh base
182
+ // - Finally, optimistic mutations re-applied on top (single batch)
183
+ pendingTransaction.truncate = true
184
+ },
185
+ })
186
+ )
177
187
 
178
188
  // Store cleanup function if provided
179
- this.syncCleanupFn = typeof cleanupFn === `function` ? cleanupFn : null
189
+ this.syncCleanupFn = syncRes?.cleanup ?? null
190
+
191
+ // Store onLoadMore function if provided
192
+ this.syncOnLoadMoreFn = syncRes?.onLoadMore ?? null
180
193
  } catch (error) {
181
194
  this.lifecycle.setStatus(`error`)
182
195
  throw error
@@ -225,6 +238,18 @@ export class CollectionSyncManager<
225
238
  return this.preloadPromise
226
239
  }
227
240
 
241
+ /**
242
+ * Requests the sync layer to load more data.
243
+ * @param options Options to control what data is being loaded
244
+ * @returns If data loading is asynchronous, this method returns a promise that resolves when the data is loaded.
245
+ * If data loading is synchronous, the data is loaded when the method returns.
246
+ */
247
+ public syncMore(options: OnLoadMoreOptions): void | Promise<void> {
248
+ if (this.syncOnLoadMoreFn) {
249
+ return this.syncOnLoadMoreFn(options)
250
+ }
251
+ }
252
+
228
253
  public cleanup(): void {
229
254
  try {
230
255
  if (this.syncCleanupFn) {
@@ -248,3 +273,15 @@ export class CollectionSyncManager<
248
273
  this.preloadPromise = null
249
274
  }
250
275
  }
276
+
277
+ function normalizeSyncFnResult(result: void | CleanupFn | SyncConfigRes) {
278
+ if (typeof result === `function`) {
279
+ return { cleanup: result }
280
+ }
281
+
282
+ if (typeof result === `object`) {
283
+ return result
284
+ }
285
+
286
+ return undefined
287
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  // Re-export all public APIs
2
+ // Re-export IR types under their own namespace
3
+ // because custom collections need access to the IR types
4
+ import * as IR from "./query/ir.js"
5
+
2
6
  export * from "./collection/index.js"
3
7
  export * from "./SortedMap"
4
8
  export * from "./transactions"
@@ -18,3 +22,4 @@ export { type IndexOptions } from "./indexes/index-options.js"
18
22
 
19
23
  // Re-export some stuff explicitly to ensure the type & value is exported
20
24
  export type { Collection } from "./collection/index.js"
25
+ export { IR }
@@ -58,7 +58,10 @@ export function ensureIndexForField<
58
58
  options: compareFn ? { compareFn, compareOptions } : {},
59
59
  })
60
60
  } catch (error) {
61
- console.warn(`Failed to create auto-index for field "${fieldName}":`, error)
61
+ console.warn(
62
+ `${collection.id ? `[${collection.id}] ` : ``}Failed to create auto-index for field "${fieldName}":`,
63
+ error
64
+ )
62
65
  }
63
66
  }
64
67
 
@@ -1,5 +1,5 @@
1
1
  import { Func, PropRef, Value } from "../ir.js"
2
- import type { BasicExpression } from "../ir.js"
2
+ import type { BasicExpression, OrderBy } from "../ir.js"
3
3
 
4
4
  /**
5
5
  * Functions supported by the collection index system.
@@ -90,3 +90,28 @@ export function convertToBasicExpression(
90
90
  return new Func(whereClause.name, args)
91
91
  }
92
92
  }
93
+
94
+ export function convertOrderByToBasicExpression(
95
+ orderBy: OrderBy,
96
+ collectionAlias: string
97
+ ): OrderBy {
98
+ const normalizedOrderBy = orderBy.map((clause) => {
99
+ const basicExp = convertToBasicExpression(
100
+ clause.expression,
101
+ collectionAlias
102
+ )
103
+
104
+ if (!basicExp) {
105
+ throw new Error(
106
+ `Failed to convert orderBy expression to a basic expression: ${clause.expression}`
107
+ )
108
+ }
109
+
110
+ return {
111
+ ...clause,
112
+ expression: basicExp,
113
+ }
114
+ })
115
+
116
+ return normalizedOrderBy
117
+ }
@@ -6,13 +6,14 @@ import { findIndexForField } from "../../utils/index-optimization.js"
6
6
  import { compileExpression } from "./evaluators.js"
7
7
  import { replaceAggregatesByRefs } from "./group-by.js"
8
8
  import type { CompiledSingleRowExpression } from "./evaluators.js"
9
- import type { OrderByClause, QueryIR, Select } from "../ir.js"
9
+ import type { OrderBy, OrderByClause, QueryIR, Select } from "../ir.js"
10
10
  import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
11
11
  import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm"
12
12
  import type { IndexInterface } from "../../indexes/base-index.js"
13
13
  import type { Collection } from "../../collection/index.js"
14
14
 
15
15
  export type OrderByOptimizationInfo = {
16
+ orderBy: OrderBy
16
17
  offset: number
17
18
  limit: number
18
19
  comparator: (
@@ -160,6 +161,7 @@ export function processOrderBy(
160
161
  comparator,
161
162
  valueExtractorForRawRow,
162
163
  index,
164
+ orderBy: orderByClause,
163
165
  }
164
166
 
165
167
  optimizableOrderByCollections[followRefCollection.id] =
@@ -1,5 +1,8 @@
1
1
  import { MultiSet } from "@tanstack/db-ivm"
2
- import { convertToBasicExpression } from "../compiler/expressions.js"
2
+ import {
3
+ convertOrderByToBasicExpression,
4
+ convertToBasicExpression,
5
+ } from "../compiler/expressions.js"
3
6
  import type { FullSyncState } from "./types.js"
4
7
  import type { MultiSetArray, RootStreamBuilder } from "@tanstack/db-ivm"
5
8
  import type { Collection } from "../../collection/index.js"
@@ -16,26 +19,29 @@ export class CollectionSubscriber<
16
19
  // Keep track of the biggest value we've sent so far (needed for orderBy optimization)
17
20
  private biggest: any = undefined
18
21
 
22
+ private collectionAlias: string
23
+
19
24
  constructor(
20
25
  private collectionId: string,
21
26
  private collection: Collection,
22
27
  private config: Parameters<SyncConfig<TResult>[`sync`]>[0],
23
28
  private syncState: FullSyncState,
24
29
  private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>
25
- ) {}
26
-
27
- subscribe(): CollectionSubscription {
28
- const collectionAlias = findCollectionAlias(
30
+ ) {
31
+ this.collectionAlias = findCollectionAlias(
29
32
  this.collectionId,
30
33
  this.collectionConfigBuilder.query
31
- )
32
- const whereClause = this.getWhereClauseFromAlias(collectionAlias)
34
+ )!
35
+ }
36
+
37
+ subscribe(): CollectionSubscription {
38
+ const whereClause = this.getWhereClauseFromAlias(this.collectionAlias)
33
39
 
34
40
  if (whereClause) {
35
41
  // Convert WHERE clause to BasicExpression format for collection subscription
36
42
  const whereExpression = convertToBasicExpression(
37
43
  whereClause,
38
- collectionAlias!
44
+ this.collectionAlias
39
45
  )
40
46
 
41
47
  if (whereExpression) {
@@ -128,7 +134,7 @@ export class CollectionSubscriber<
128
134
  private subscribeToOrderedChanges(
129
135
  whereExpression: BasicExpression<boolean> | undefined
130
136
  ) {
131
- const { offset, limit, comparator, dataNeeded, index } =
137
+ const { orderBy, offset, limit, comparator, dataNeeded, index } =
132
138
  this.collectionConfigBuilder.optimizableOrderByCollections[
133
139
  this.collectionId
134
140
  ]!
@@ -164,10 +170,17 @@ export class CollectionSubscriber<
164
170
 
165
171
  subscription.setOrderByIndex(index)
166
172
 
173
+ // Normalize the orderBy clauses such that the references are relative to the collection
174
+ const normalizedOrderBy = convertOrderByToBasicExpression(
175
+ orderBy,
176
+ this.collectionAlias
177
+ )
178
+
167
179
  // Load the first `offset + limit` values from the index
168
180
  // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[
169
181
  subscription.requestLimitedSnapshot({
170
182
  limit: offset + limit,
183
+ orderBy: normalizedOrderBy,
171
184
  })
172
185
 
173
186
  return subscription
@@ -225,7 +238,7 @@ export class CollectionSubscriber<
225
238
  // Loads the next `n` items from the collection
226
239
  // starting from the biggest item it has sent
227
240
  private loadNextItems(n: number, subscription: CollectionSubscription) {
228
- const { valueExtractorForRawRow } =
241
+ const { orderBy, valueExtractorForRawRow } =
229
242
  this.collectionConfigBuilder.optimizableOrderByCollections[
230
243
  this.collectionId
231
244
  ]!
@@ -233,8 +246,16 @@ export class CollectionSubscriber<
233
246
  const biggestSentValue = biggestSentRow
234
247
  ? valueExtractorForRawRow(biggestSentRow)
235
248
  : biggestSentRow
249
+
250
+ // Normalize the orderBy clauses such that the references are relative to the collection
251
+ const normalizedOrderBy = convertOrderByToBasicExpression(
252
+ orderBy,
253
+ this.collectionAlias
254
+ )
255
+
236
256
  // Take the `n` items after the biggest sent value
237
257
  subscription.requestLimitedSnapshot({
258
+ orderBy: normalizedOrderBy,
238
259
  limit: n,
239
260
  minValue: biggestSentValue,
240
261
  })
package/src/types.ts CHANGED
@@ -150,6 +150,18 @@ export type Row<TExtensions = never> = Record<string, Value<TExtensions>>
150
150
 
151
151
  export type OperationType = `insert` | `update` | `delete`
152
152
 
153
+ export type OnLoadMoreOptions = {
154
+ where?: BasicExpression<boolean>
155
+ orderBy?: OrderBy
156
+ limit?: number
157
+ }
158
+
159
+ export type CleanupFn = () => void
160
+
161
+ export type SyncConfigRes = {
162
+ cleanup?: CleanupFn
163
+ onLoadMore?: (options: OnLoadMoreOptions) => void | Promise<void>
164
+ }
153
165
  export interface SyncConfig<
154
166
  T extends object = Record<string, unknown>,
155
167
  TKey extends string | number = string | number,
@@ -161,7 +173,7 @@ export interface SyncConfig<
161
173
  commit: () => void
162
174
  markReady: () => void
163
175
  truncate: () => void
164
- }) => void
176
+ }) => void | CleanupFn | SyncConfigRes
165
177
 
166
178
  /**
167
179
  * Get the sync metadata for insert operations