@tanstack/db 0.4.9 → 0.4.11

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 (100) hide show
  1. package/dist/cjs/collection/events.cjs +9 -51
  2. package/dist/cjs/collection/events.cjs.map +1 -1
  3. package/dist/cjs/collection/events.d.cts +18 -7
  4. package/dist/cjs/collection/index.cjs +9 -12
  5. package/dist/cjs/collection/index.cjs.map +1 -1
  6. package/dist/cjs/collection/index.d.cts +13 -14
  7. package/dist/cjs/collection/subscription.cjs +62 -6
  8. package/dist/cjs/collection/subscription.cjs.map +1 -1
  9. package/dist/cjs/collection/subscription.d.cts +16 -3
  10. package/dist/cjs/collection/sync.cjs +58 -6
  11. package/dist/cjs/collection/sync.cjs.map +1 -1
  12. package/dist/cjs/collection/sync.d.cts +18 -4
  13. package/dist/cjs/errors.cjs +8 -0
  14. package/dist/cjs/errors.cjs.map +1 -1
  15. package/dist/cjs/errors.d.cts +6 -0
  16. package/dist/cjs/event-emitter.cjs +94 -0
  17. package/dist/cjs/event-emitter.cjs.map +1 -0
  18. package/dist/cjs/event-emitter.d.cts +45 -0
  19. package/dist/cjs/index.cjs +1 -0
  20. package/dist/cjs/index.cjs.map +1 -1
  21. package/dist/cjs/local-only.cjs.map +1 -1
  22. package/dist/cjs/local-only.d.cts +2 -5
  23. package/dist/cjs/query/compiler/index.cjs +6 -2
  24. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  25. package/dist/cjs/query/compiler/index.d.cts +3 -2
  26. package/dist/cjs/query/compiler/joins.cjs +6 -3
  27. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  28. package/dist/cjs/query/compiler/joins.d.cts +2 -2
  29. package/dist/cjs/query/compiler/order-by.cjs +18 -4
  30. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  31. package/dist/cjs/query/compiler/order-by.d.cts +2 -1
  32. package/dist/cjs/query/compiler/types.d.cts +4 -0
  33. package/dist/cjs/query/index.d.cts +1 -0
  34. package/dist/cjs/query/live/collection-config-builder.cjs +43 -6
  35. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  36. package/dist/cjs/query/live/collection-config-builder.d.cts +27 -1
  37. package/dist/cjs/query/live/collection-subscriber.cjs +29 -0
  38. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  39. package/dist/cjs/query/live/collection-subscriber.d.cts +1 -0
  40. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  41. package/dist/cjs/query/live-query-collection.d.cts +2 -2
  42. package/dist/cjs/types.d.cts +82 -11
  43. package/dist/esm/collection/events.d.ts +18 -7
  44. package/dist/esm/collection/events.js +9 -51
  45. package/dist/esm/collection/events.js.map +1 -1
  46. package/dist/esm/collection/index.d.ts +13 -14
  47. package/dist/esm/collection/index.js +9 -12
  48. package/dist/esm/collection/index.js.map +1 -1
  49. package/dist/esm/collection/subscription.d.ts +16 -3
  50. package/dist/esm/collection/subscription.js +62 -6
  51. package/dist/esm/collection/subscription.js.map +1 -1
  52. package/dist/esm/collection/sync.d.ts +18 -4
  53. package/dist/esm/collection/sync.js +59 -7
  54. package/dist/esm/collection/sync.js.map +1 -1
  55. package/dist/esm/errors.d.ts +6 -0
  56. package/dist/esm/errors.js +8 -0
  57. package/dist/esm/errors.js.map +1 -1
  58. package/dist/esm/event-emitter.d.ts +45 -0
  59. package/dist/esm/event-emitter.js +94 -0
  60. package/dist/esm/event-emitter.js.map +1 -0
  61. package/dist/esm/index.js +2 -1
  62. package/dist/esm/local-only.d.ts +2 -5
  63. package/dist/esm/local-only.js.map +1 -1
  64. package/dist/esm/query/compiler/index.d.ts +3 -2
  65. package/dist/esm/query/compiler/index.js +6 -2
  66. package/dist/esm/query/compiler/index.js.map +1 -1
  67. package/dist/esm/query/compiler/joins.d.ts +2 -2
  68. package/dist/esm/query/compiler/joins.js +6 -3
  69. package/dist/esm/query/compiler/joins.js.map +1 -1
  70. package/dist/esm/query/compiler/order-by.d.ts +2 -1
  71. package/dist/esm/query/compiler/order-by.js +18 -4
  72. package/dist/esm/query/compiler/order-by.js.map +1 -1
  73. package/dist/esm/query/compiler/types.d.ts +4 -0
  74. package/dist/esm/query/index.d.ts +1 -0
  75. package/dist/esm/query/live/collection-config-builder.d.ts +27 -1
  76. package/dist/esm/query/live/collection-config-builder.js +44 -7
  77. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  78. package/dist/esm/query/live/collection-subscriber.d.ts +1 -0
  79. package/dist/esm/query/live/collection-subscriber.js +29 -0
  80. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  81. package/dist/esm/query/live-query-collection.d.ts +2 -2
  82. package/dist/esm/query/live-query-collection.js.map +1 -1
  83. package/dist/esm/types.d.ts +82 -11
  84. package/package.json +2 -2
  85. package/src/collection/events.ts +25 -74
  86. package/src/collection/index.ts +15 -19
  87. package/src/collection/subscription.ts +88 -6
  88. package/src/collection/sync.ts +81 -9
  89. package/src/errors.ts +12 -0
  90. package/src/event-emitter.ts +118 -0
  91. package/src/local-only.ts +5 -12
  92. package/src/query/compiler/index.ts +9 -1
  93. package/src/query/compiler/joins.ts +7 -1
  94. package/src/query/compiler/order-by.ts +23 -2
  95. package/src/query/compiler/types.ts +5 -0
  96. package/src/query/index.ts +1 -0
  97. package/src/query/live/collection-config-builder.ts +76 -7
  98. package/src/query/live/collection-subscriber.ts +50 -0
  99. package/src/query/live-query-collection.ts +8 -4
  100. package/src/types.ts +93 -11
@@ -3,6 +3,7 @@ import { Collection } from './collection/index.js';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
4
  import { Transaction } from './transactions.js';
5
5
  import { BasicExpression, OrderBy } from './query/ir.js';
6
+ import { EventEmitter } from './event-emitter.js';
6
7
  /**
7
8
  * Helper type to extract the output type from a standard schema
8
9
  *
@@ -92,15 +93,74 @@ type Value<TExtensions = never> = string | number | boolean | bigint | null | TE
92
93
  };
93
94
  export type Row<TExtensions = never> = Record<string, Value<TExtensions>>;
94
95
  export type OperationType = `insert` | `update` | `delete`;
95
- export type OnLoadMoreOptions = {
96
+ /**
97
+ * Subscription status values
98
+ */
99
+ export type SubscriptionStatus = `ready` | `loadingSubset`;
100
+ /**
101
+ * Event emitted when subscription status changes
102
+ */
103
+ export interface SubscriptionStatusChangeEvent {
104
+ type: `status:change`;
105
+ subscription: Subscription;
106
+ previousStatus: SubscriptionStatus;
107
+ status: SubscriptionStatus;
108
+ }
109
+ /**
110
+ * Event emitted when subscription status changes to a specific status
111
+ */
112
+ export interface SubscriptionStatusEvent<T extends SubscriptionStatus> {
113
+ type: `status:${T}`;
114
+ subscription: Subscription;
115
+ previousStatus: SubscriptionStatus;
116
+ status: T;
117
+ }
118
+ /**
119
+ * Event emitted when subscription is unsubscribed
120
+ */
121
+ export interface SubscriptionUnsubscribedEvent {
122
+ type: `unsubscribed`;
123
+ subscription: Subscription;
124
+ }
125
+ /**
126
+ * All subscription events
127
+ */
128
+ export type SubscriptionEvents = {
129
+ "status:change": SubscriptionStatusChangeEvent;
130
+ "status:ready": SubscriptionStatusEvent<`ready`>;
131
+ "status:loadingSubset": SubscriptionStatusEvent<`loadingSubset`>;
132
+ unsubscribed: SubscriptionUnsubscribedEvent;
133
+ };
134
+ /**
135
+ * Public interface for a collection subscription
136
+ * Used by sync implementations to track subscription lifecycle
137
+ */
138
+ export interface Subscription extends EventEmitter<SubscriptionEvents> {
139
+ /** Current status of the subscription */
140
+ readonly status: SubscriptionStatus;
141
+ }
142
+ export type LoadSubsetOptions = {
143
+ /** The where expression to filter the data */
96
144
  where?: BasicExpression<boolean>;
145
+ /** The order by clause to sort the data */
97
146
  orderBy?: OrderBy;
147
+ /** The limit of the data to load */
98
148
  limit?: number;
149
+ /**
150
+ * The subscription that triggered the load.
151
+ * Advanced sync implementations can use this for:
152
+ * - LRU caching keyed by subscription
153
+ * - Reference counting to track active subscriptions
154
+ * - Subscribing to subscription events (e.g., finalization/unsubscribe)
155
+ * @optional Available when called from CollectionSubscription, may be undefined for direct calls
156
+ */
157
+ subscription?: Subscription;
99
158
  };
159
+ export type LoadSubsetFn = (options: LoadSubsetOptions) => true | Promise<void>;
100
160
  export type CleanupFn = () => void;
101
161
  export type SyncConfigRes = {
102
162
  cleanup?: CleanupFn;
103
- onLoadMore?: (options: OnLoadMoreOptions) => void | Promise<void>;
163
+ loadSubset?: LoadSubsetFn;
104
164
  };
105
165
  export interface SyncConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number> {
106
166
  sync: (params: {
@@ -161,21 +221,21 @@ export interface InsertConfig {
161
221
  /** Whether to apply optimistic updates immediately. Defaults to true. */
162
222
  optimistic?: boolean;
163
223
  }
164
- export type UpdateMutationFnParams<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>> = {
224
+ export type UpdateMutationFnParams<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord> = {
165
225
  transaction: TransactionWithMutations<T, `update`>;
166
226
  collection: Collection<T, TKey, TUtils>;
167
227
  };
168
- export type InsertMutationFnParams<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>> = {
228
+ export type InsertMutationFnParams<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord> = {
169
229
  transaction: TransactionWithMutations<T, `insert`>;
170
230
  collection: Collection<T, TKey, TUtils>;
171
231
  };
172
- export type DeleteMutationFnParams<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>> = {
232
+ export type DeleteMutationFnParams<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord> = {
173
233
  transaction: TransactionWithMutations<T, `delete`>;
174
234
  collection: Collection<T, TKey, TUtils>;
175
235
  };
176
- export type InsertMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>, TReturn = any> = (params: InsertMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>;
177
- export type UpdateMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>, TReturn = any> = (params: UpdateMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>;
178
- export type DeleteMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = Record<string, Fn>, TReturn = any> = (params: DeleteMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>;
236
+ export type InsertMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, TReturn = any> = (params: InsertMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>;
237
+ export type UpdateMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, TReturn = any> = (params: UpdateMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>;
238
+ export type DeleteMutationFn<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TUtils extends UtilsRecord = UtilsRecord, TReturn = any> = (params: DeleteMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>;
179
239
  /**
180
240
  * Collection status values for lifecycle management
181
241
  * @example
@@ -202,7 +262,8 @@ export type CollectionStatus =
202
262
  | `error`
203
263
  /** Collection has been cleaned up and resources freed */
204
264
  | `cleaned-up`;
205
- export interface BaseCollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = Record<string, Fn>, TReturn = any> {
265
+ export type SyncMode = `eager` | `on-demand`;
266
+ export interface BaseCollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = UtilsRecord, TReturn = any> {
206
267
  id?: string;
207
268
  schema?: TSchema;
208
269
  /**
@@ -251,6 +312,15 @@ export interface BaseCollectionConfig<T extends object = Record<string, unknown>
251
312
  * compare: (x, y) => x.createdAt.getTime() - y.createdAt.getTime()
252
313
  */
253
314
  compare?: (x: T, y: T) => number;
315
+ /**
316
+ * The mode of sync to use for the collection.
317
+ * @default `eager`
318
+ * @description
319
+ * - `eager`: syncs all data immediately on preload
320
+ * - `on-demand`: syncs data in incremental snapshots when the collection is queried
321
+ * The exact implementation of the sync mode is up to the sync implementation.
322
+ */
323
+ syncMode?: SyncMode;
254
324
  /**
255
325
  * Optional asynchronous handler function called before an insert operation
256
326
  * @param params Object containing transaction and collection information
@@ -379,8 +449,9 @@ export interface BaseCollectionConfig<T extends object = Record<string, unknown>
379
449
  * }
380
450
  */
381
451
  onDelete?: DeleteMutationFn<T, TKey, TUtils, TReturn>;
452
+ utils?: TUtils;
382
453
  }
383
- export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never> extends BaseCollectionConfig<T, TKey, TSchema> {
454
+ export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = UtilsRecord> extends BaseCollectionConfig<T, TKey, TSchema, TUtils> {
384
455
  sync: SyncConfig<T, TKey>;
385
456
  }
386
457
  export type SingleResult = {
@@ -395,7 +466,7 @@ export type MaybeSingleResult = {
395
466
  */
396
467
  singleResult?: true;
397
468
  };
398
- export type CollectionConfigSingleRowOption<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never> = CollectionConfig<T, TKey, TSchema> & MaybeSingleResult;
469
+ export type CollectionConfigSingleRowOption<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never, TUtils extends UtilsRecord = {}> = CollectionConfig<T, TKey, TSchema, TUtils> & MaybeSingleResult;
399
470
  export type ChangesPayload<T extends object = Record<string, unknown>> = Array<ChangeMessage<T>>;
400
471
  /**
401
472
  * An input row from a collection
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
3
  "description": "A reactive client store for building super fast apps on sync",
4
- "version": "0.4.9",
4
+ "version": "0.4.11",
5
5
  "dependencies": {
6
6
  "@standard-schema/spec": "^1.0.0",
7
- "@tanstack/db-ivm": "0.1.10"
7
+ "@tanstack/db-ivm": "0.1.11"
8
8
  },
9
9
  "devDependencies": {
10
10
  "@vitest/coverage-istanbul": "^3.2.4",
@@ -1,3 +1,4 @@
1
+ import { EventEmitter } from "../event-emitter.js"
1
2
  import type { Collection } from "./index.js"
2
3
  import type { CollectionStatus } from "../types.js"
3
4
 
@@ -31,9 +32,21 @@ export interface CollectionSubscribersChangeEvent {
31
32
  subscriberCount: number
32
33
  }
33
34
 
35
+ /**
36
+ * Event emitted when the collection's loading more state changes
37
+ */
38
+ export interface CollectionLoadingSubsetChangeEvent {
39
+ type: `loadingSubset:change`
40
+ collection: Collection<any, any, any, any, any>
41
+ isLoadingSubset: boolean
42
+ previousIsLoadingSubset: boolean
43
+ loadingSubsetTransition: `start` | `end`
44
+ }
45
+
34
46
  export type AllCollectionEvents = {
35
47
  "status:change": CollectionStatusChangeEvent
36
48
  "subscribers:change": CollectionSubscribersChangeEvent
49
+ "loadingSubset:change": CollectionLoadingSubsetChangeEvent
37
50
  } & {
38
51
  [K in CollectionStatus as `status:${K}`]: CollectionStatusEvent<K>
39
52
  }
@@ -42,94 +55,32 @@ export type CollectionEvent =
42
55
  | AllCollectionEvents[keyof AllCollectionEvents]
43
56
  | CollectionStatusChangeEvent
44
57
  | CollectionSubscribersChangeEvent
58
+ | CollectionLoadingSubsetChangeEvent
45
59
 
46
60
  export type CollectionEventHandler<T extends keyof AllCollectionEvents> = (
47
61
  event: AllCollectionEvents[T]
48
62
  ) => void
49
63
 
50
- export class CollectionEventsManager {
64
+ export class CollectionEventsManager extends EventEmitter<AllCollectionEvents> {
51
65
  private collection!: Collection<any, any, any, any, any>
52
- private listeners = new Map<
53
- keyof AllCollectionEvents,
54
- Set<CollectionEventHandler<any>>
55
- >()
56
66
 
57
- constructor() {}
67
+ constructor() {
68
+ super()
69
+ }
58
70
 
59
71
  setDeps(deps: { collection: Collection<any, any, any, any, any> }) {
60
72
  this.collection = deps.collection
61
73
  }
62
74
 
63
- on<T extends keyof AllCollectionEvents>(
64
- event: T,
65
- callback: CollectionEventHandler<T>
66
- ) {
67
- if (!this.listeners.has(event)) {
68
- this.listeners.set(event, new Set())
69
- }
70
- this.listeners.get(event)!.add(callback)
71
-
72
- return () => {
73
- this.listeners.get(event)?.delete(callback)
74
- }
75
- }
76
-
77
- once<T extends keyof AllCollectionEvents>(
78
- event: T,
79
- callback: CollectionEventHandler<T>
80
- ) {
81
- const unsubscribe = this.on(event, (eventPayload) => {
82
- callback(eventPayload)
83
- unsubscribe()
84
- })
85
- return unsubscribe
86
- }
87
-
88
- off<T extends keyof AllCollectionEvents>(
89
- event: T,
90
- callback: CollectionEventHandler<T>
91
- ) {
92
- this.listeners.get(event)?.delete(callback)
93
- }
94
-
95
- waitFor<T extends keyof AllCollectionEvents>(
96
- event: T,
97
- timeout?: number
98
- ): Promise<AllCollectionEvents[T]> {
99
- return new Promise((resolve, reject) => {
100
- let timeoutId: NodeJS.Timeout | undefined
101
- const unsubscribe = this.on(event, (eventPayload) => {
102
- if (timeoutId) {
103
- clearTimeout(timeoutId)
104
- timeoutId = undefined
105
- }
106
- resolve(eventPayload)
107
- unsubscribe()
108
- })
109
- if (timeout) {
110
- timeoutId = setTimeout(() => {
111
- timeoutId = undefined
112
- unsubscribe()
113
- reject(new Error(`Timeout waiting for event ${event}`))
114
- }, timeout)
115
- }
116
- })
117
- }
118
-
75
+ /**
76
+ * Emit an event to all listeners
77
+ * Public API for emitting collection events
78
+ */
119
79
  emit<T extends keyof AllCollectionEvents>(
120
80
  event: T,
121
81
  eventPayload: AllCollectionEvents[T]
122
- ) {
123
- this.listeners.get(event)?.forEach((listener) => {
124
- try {
125
- listener(eventPayload)
126
- } catch (error) {
127
- // Re-throw in a microtask to surface the error
128
- queueMicrotask(() => {
129
- throw error
130
- })
131
- }
132
- })
82
+ ): void {
83
+ this.emitInner(event, eventPayload)
133
84
  }
134
85
 
135
86
  emitStatusChange<T extends CollectionStatus>(
@@ -166,6 +117,6 @@ export class CollectionEventsManager {
166
117
  }
167
118
 
168
119
  cleanup() {
169
- this.listeners.clear()
120
+ this.clearListeners()
170
121
  }
171
122
  }
@@ -25,7 +25,6 @@ import type {
25
25
  InferSchemaOutput,
26
26
  InsertConfig,
27
27
  NonSingleResult,
28
- OnLoadMoreOptions,
29
28
  OperationConfig,
30
29
  SingleResult,
31
30
  SubscribeChangesOptions,
@@ -48,7 +47,7 @@ import type { IndexProxy } from "../indexes/lazy-index.js"
48
47
  export interface Collection<
49
48
  T extends object = Record<string, unknown>,
50
49
  TKey extends string | number = string | number,
51
- TUtils extends UtilsRecord = {},
50
+ TUtils extends UtilsRecord = UtilsRecord,
52
51
  TSchema extends StandardSchemaV1 = StandardSchemaV1,
53
52
  TInsertInput extends object = T,
54
53
  > extends CollectionImpl<T, TKey, TUtils, TSchema, TInsertInput> {
@@ -131,7 +130,7 @@ export interface Collection<
131
130
  export function createCollection<
132
131
  T extends StandardSchemaV1,
133
132
  TKey extends string | number = string | number,
134
- TUtils extends UtilsRecord = {},
133
+ TUtils extends UtilsRecord = UtilsRecord,
135
134
  >(
136
135
  options: CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
137
136
  schema: T
@@ -144,7 +143,7 @@ export function createCollection<
144
143
  export function createCollection<
145
144
  T extends StandardSchemaV1,
146
145
  TKey extends string | number = string | number,
147
- TUtils extends UtilsRecord = {},
146
+ TUtils extends UtilsRecord = UtilsRecord,
148
147
  >(
149
148
  options: CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
150
149
  schema: T
@@ -158,7 +157,7 @@ export function createCollection<
158
157
  export function createCollection<
159
158
  T extends object,
160
159
  TKey extends string | number = string | number,
161
- TUtils extends UtilsRecord = {},
160
+ TUtils extends UtilsRecord = UtilsRecord,
162
161
  >(
163
162
  options: CollectionConfig<T, TKey, never> & {
164
163
  schema?: never // prohibit schema if an explicit type is provided
@@ -171,7 +170,7 @@ export function createCollection<
171
170
  export function createCollection<
172
171
  T extends object,
173
172
  TKey extends string | number = string | number,
174
- TUtils extends UtilsRecord = {},
173
+ TUtils extends UtilsRecord = UtilsRecord,
175
174
  >(
176
175
  options: CollectionConfig<T, TKey, never> & {
177
176
  schema?: never // prohibit schema if an explicit type is provided
@@ -218,7 +217,7 @@ export class CollectionImpl<
218
217
  private _events: CollectionEventsManager
219
218
  private _changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>
220
219
  public _lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
221
- private _sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
220
+ public _sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>
222
221
  private _indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>
223
222
  private _mutations: CollectionMutationsManager<
224
223
  TOutput,
@@ -303,6 +302,7 @@ export class CollectionImpl<
303
302
  collection: this, // Required for passing to config.sync callback
304
303
  state: this._state,
305
304
  lifecycle: this._lifecycle,
305
+ events: this._events,
306
306
  })
307
307
 
308
308
  // Only start sync immediately if explicitly enabled
@@ -356,23 +356,19 @@ export class CollectionImpl<
356
356
  }
357
357
 
358
358
  /**
359
- * Start sync immediately - internal method for compiled queries
360
- * This bypasses lazy loading for special cases like live query results
359
+ * Check if the collection is currently loading more data
360
+ * @returns true if the collection has pending load more operations, false otherwise
361
361
  */
362
- public startSyncImmediate(): void {
363
- this._sync.startSync()
362
+ public get isLoadingSubset(): boolean {
363
+ return this._sync.isLoadingSubset
364
364
  }
365
365
 
366
366
  /**
367
- * Requests the sync layer to load more data.
368
- * @param options Options to control what data is being loaded
369
- * @returns If data loading is asynchronous, this method returns a promise that resolves when the data is loaded.
370
- * If data loading is synchronous, the data is loaded when the method returns.
367
+ * Start sync immediately - internal method for compiled queries
368
+ * This bypasses lazy loading for special cases like live query results
371
369
  */
372
- public syncMore(options: OnLoadMoreOptions): void | Promise<void> {
373
- if (this._sync.syncOnLoadMoreFn) {
374
- return this._sync.syncOnLoadMoreFn(options)
375
- }
370
+ public startSyncImmediate(): void {
371
+ this._sync.startSync()
376
372
  }
377
373
 
378
374
  /**
@@ -1,13 +1,20 @@
1
1
  import { ensureIndexForExpression } from "../indexes/auto-index.js"
2
2
  import { and, gt, lt } from "../query/builder/functions.js"
3
3
  import { Value } from "../query/ir.js"
4
+ import { EventEmitter } from "../event-emitter.js"
4
5
  import {
5
6
  createFilterFunctionFromExpression,
6
7
  createFilteredCallback,
7
8
  } from "./change-events.js"
8
9
  import type { BasicExpression, OrderBy } from "../query/ir.js"
9
10
  import type { IndexInterface } from "../indexes/base-index.js"
10
- import type { ChangeMessage } from "../types.js"
11
+ import type {
12
+ ChangeMessage,
13
+ Subscription,
14
+ SubscriptionEvents,
15
+ SubscriptionStatus,
16
+ SubscriptionUnsubscribedEvent,
17
+ } from "../types.js"
11
18
  import type { CollectionImpl } from "./index.js"
12
19
 
13
20
  type RequestSnapshotOptions = {
@@ -22,13 +29,17 @@ type RequestLimitedSnapshotOptions = {
22
29
  }
23
30
 
24
31
  type CollectionSubscriptionOptions = {
32
+ includeInitialState?: boolean
25
33
  /** Pre-compiled expression for filtering changes */
26
34
  whereExpression?: BasicExpression<boolean>
27
35
  /** Callback to call when the subscription is unsubscribed */
28
- onUnsubscribe?: () => void
36
+ onUnsubscribe?: (event: SubscriptionUnsubscribedEvent) => void
29
37
  }
30
38
 
31
- export class CollectionSubscription {
39
+ export class CollectionSubscription
40
+ extends EventEmitter<SubscriptionEvents>
41
+ implements Subscription
42
+ {
32
43
  private loadedInitialState = false
33
44
 
34
45
  // Flag to indicate that we have sent at least 1 snapshot.
@@ -42,11 +53,24 @@ export class CollectionSubscription {
42
53
 
43
54
  private orderByIndex: IndexInterface<string | number> | undefined
44
55
 
56
+ // Status tracking
57
+ private _status: SubscriptionStatus = `ready`
58
+ private pendingLoadSubsetPromises: Set<Promise<void>> = new Set()
59
+
60
+ public get status(): SubscriptionStatus {
61
+ return this._status
62
+ }
63
+
45
64
  constructor(
46
65
  private collection: CollectionImpl<any, any, any, any, any>,
47
66
  private callback: (changes: Array<ChangeMessage<any, any>>) => void,
48
67
  private options: CollectionSubscriptionOptions
49
68
  ) {
69
+ super()
70
+ if (options.onUnsubscribe) {
71
+ this.on(`unsubscribed`, (event) => options.onUnsubscribe!(event))
72
+ }
73
+
50
74
  // Auto-index for where expressions if enabled
51
75
  if (options.whereExpression) {
52
76
  ensureIndexForExpression(options.whereExpression, this.collection)
@@ -71,6 +95,53 @@ export class CollectionSubscription {
71
95
  this.orderByIndex = index
72
96
  }
73
97
 
98
+ /**
99
+ * Set subscription status and emit events if changed
100
+ */
101
+ private setStatus(newStatus: SubscriptionStatus) {
102
+ if (this._status === newStatus) {
103
+ return // No change
104
+ }
105
+
106
+ const previousStatus = this._status
107
+ this._status = newStatus
108
+
109
+ // Emit status:change event
110
+ this.emitInner(`status:change`, {
111
+ type: `status:change`,
112
+ subscription: this,
113
+ previousStatus,
114
+ status: newStatus,
115
+ })
116
+
117
+ // Emit specific status event
118
+ const eventKey: `status:${SubscriptionStatus}` = `status:${newStatus}`
119
+ this.emitInner(eventKey, {
120
+ type: eventKey,
121
+ subscription: this,
122
+ previousStatus,
123
+ status: newStatus,
124
+ } as SubscriptionEvents[typeof eventKey])
125
+ }
126
+
127
+ /**
128
+ * Track a loadSubset promise and manage loading status
129
+ */
130
+ private trackLoadSubsetPromise(syncResult: Promise<void> | true) {
131
+ // Track the promise if it's actually a promise (async work)
132
+ if (syncResult instanceof Promise) {
133
+ this.pendingLoadSubsetPromises.add(syncResult)
134
+ this.setStatus(`loadingSubset`)
135
+
136
+ syncResult.finally(() => {
137
+ this.pendingLoadSubsetPromises.delete(syncResult)
138
+ if (this.pendingLoadSubsetPromises.size === 0) {
139
+ this.setStatus(`ready`)
140
+ }
141
+ })
142
+ }
143
+ }
144
+
74
145
  hasLoadedInitialState() {
75
146
  return this.loadedInitialState
76
147
  }
@@ -121,10 +192,13 @@ export class CollectionSubscription {
121
192
 
122
193
  // Request the sync layer to load more data
123
194
  // don't await it, we will load the data into the collection when it comes in
124
- this.collection.syncMore({
195
+ const syncResult = this.collection._sync.loadSubset({
125
196
  where: stateOpts.where,
197
+ subscription: this,
126
198
  })
127
199
 
200
+ this.trackLoadSubsetPromise(syncResult)
201
+
128
202
  // Also load data immediately from the collection
129
203
  const snapshot = this.collection.currentStateAsChanges(stateOpts)
130
204
 
@@ -215,11 +289,14 @@ export class CollectionSubscription {
215
289
 
216
290
  // Request the sync layer to load more data
217
291
  // don't await it, we will load the data into the collection when it comes in
218
- this.collection.syncMore({
292
+ const syncResult = this.collection._sync.loadSubset({
219
293
  where: whereWithValueFilter,
220
294
  limit,
221
295
  orderBy,
296
+ subscription: this,
222
297
  })
298
+
299
+ this.trackLoadSubsetPromise(syncResult)
223
300
  }
224
301
 
225
302
  /**
@@ -264,6 +341,11 @@ export class CollectionSubscription {
264
341
  }
265
342
 
266
343
  unsubscribe() {
267
- this.options.onUnsubscribe?.()
344
+ this.emitInner(`unsubscribed`, {
345
+ type: `unsubscribed`,
346
+ subscription: this,
347
+ })
348
+ // Clear all event listeners to prevent memory leaks
349
+ this.clearListeners()
268
350
  }
269
351
  }